Skip to content

Commit d692cd6

Browse files
policyfile: support ETag parameters with escaped double quotes
Changes Set and SetAndGet for ACLs to support ETag parameters that are passed as strings enclosed in escaped double quotes, e.g. "xyzzy". The default ETag format as per the spec includes the double quotes and the API returns ETags in double quotes, however the client currently rejects ETag parameters that come in double quotes. Changing this streamlines the experience of users who want to rely on the ETag in scenarios where they read, modify and write an ACL with the client and want to ensure the ACL hasn’t been modified between reading and writing. Fixes #27 Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
1 parent 35b8e02 commit d692cd6

File tree

2 files changed

+108
-69
lines changed

2 files changed

+108
-69
lines changed

policyfile.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"net/http"
10+
"strings"
1011
"time"
1112
)
1213

@@ -198,7 +199,7 @@ func (pr *PolicyFileResource) Raw(ctx context.Context) (*RawACL, error) {
198199
func (pr *PolicyFileResource) Set(ctx context.Context, acl any, etag string) error {
199200
headers := make(map[string]string)
200201
if etag != "" {
201-
headers["If-Match"] = fmt.Sprintf("%q", etag)
202+
headers["If-Match"] = fmt.Sprintf("%q", strings.Trim(etag, `"`))
202203
}
203204

204205
reqOpts := []requestOption{
@@ -226,7 +227,7 @@ func (pr *PolicyFileResource) Set(ctx context.Context, acl any, etag string) err
226227
func (pr *PolicyFileResource) SetAndGet(ctx context.Context, acl ACL, etag string) (*ACL, error) {
227228
headers := make(map[string]string)
228229
if etag != "" {
229-
headers["If-Match"] = fmt.Sprintf("%q", etag)
230+
headers["If-Match"] = fmt.Sprintf("%q", strings.Trim(etag, `"`))
230231
}
231232

232233
reqOpts := []requestOption{

policyfile_test.go

Lines changed: 105 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -284,59 +284,78 @@ func TestClient_SetACL(t *testing.T) {
284284
}
285285

286286
func TestClient_SetAndGetACL(t *testing.T) {
287-
t.Parallel()
288-
289-
client, server := NewTestHarness(t)
290-
server.ResponseCode = http.StatusOK
291-
server.ResponseHeader.Set("ETag", "abcdefg")
292-
in := ACL{
293-
ACLs: []ACLEntry{
294-
{
295-
Action: "accept",
296-
Ports: []string{"*:*"},
297-
Users: []string{"*"},
298-
},
299-
},
300-
TagOwners: map[string][]string{
301-
"tag:example": {"group:example"},
302-
},
303-
Hosts: map[string]string{
304-
"example-host-1": "100.100.100.100",
305-
"example-host-2": "100.100.101.100/24",
306-
},
307-
Groups: map[string][]string{
308-
"group:example": {
309-
"user1@example.com",
310-
"user2@example.com",
311-
},
287+
testCases := []struct {
288+
Name string
289+
ETagParameter string
290+
RequestIfMatchHeaderValue string
291+
}{
292+
{
293+
Name: "ETag method parameter not wrapped in double quotes",
294+
ETagParameter: "test-ETag",
295+
RequestIfMatchHeaderValue: `"test-ETag"`,
312296
},
313-
Tests: []ACLTest{
314-
{
315-
User: "user1@example.com",
316-
Allow: []string{"example-host-1:22", "example-host-2:80"},
317-
Deny: []string{"exapmle-host-2:100"},
318-
},
319-
{
320-
User: "user2@example.com",
321-
Allow: []string{"100.60.3.4:22"},
322-
},
297+
{
298+
Name: "ETag method parameter wrapped in double quotes",
299+
ETagParameter: `"test-ETag"`,
300+
RequestIfMatchHeaderValue: `"test-ETag"`,
323301
},
324-
ETag: "abcdefg",
325302
}
326-
server.ResponseBody = in
327-
328-
out, err := client.PolicyFile().SetAndGet(context.Background(), in, "abcdefg")
329-
assert.NoError(t, err)
330-
assert.Equal(t, http.MethodPost, server.Method)
331-
assert.Equal(t, "/api/v2/tailnet/example.com/acl", server.Path)
332-
assert.Equal(t, `"abcdefg"`, server.Header.Get("If-Match"))
333-
assert.EqualValues(t, "application/json", server.Header.Get("Content-Type"))
334-
assert.EqualValues(t, &in, out)
335303

336-
var actualACL ACL
337-
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualACL))
338-
in.ETag = ""
339-
assert.EqualValues(t, in, actualACL)
304+
for _, tc := range testCases {
305+
t.Run(tc.Name, func(t *testing.T) {
306+
client, server := NewTestHarness(t)
307+
server.ResponseCode = http.StatusOK
308+
server.ResponseHeader.Set("ETag", tc.ETagParameter)
309+
in := ACL{
310+
ACLs: []ACLEntry{
311+
{
312+
Action: "accept",
313+
Ports: []string{"*:*"},
314+
Users: []string{"*"},
315+
},
316+
},
317+
TagOwners: map[string][]string{
318+
"tag:example": {"group:example"},
319+
},
320+
Hosts: map[string]string{
321+
"example-host-1": "100.100.100.100",
322+
"example-host-2": "100.100.101.100/24",
323+
},
324+
Groups: map[string][]string{
325+
"group:example": {
326+
"user1@example.com",
327+
"user2@example.com",
328+
},
329+
},
330+
Tests: []ACLTest{
331+
{
332+
User: "user1@example.com",
333+
Allow: []string{"example-host-1:22", "example-host-2:80"},
334+
Deny: []string{"exapmle-host-2:100"},
335+
},
336+
{
337+
User: "user2@example.com",
338+
Allow: []string{"100.60.3.4:22"},
339+
},
340+
},
341+
ETag: tc.ETagParameter,
342+
}
343+
server.ResponseBody = in
344+
345+
out, err := client.PolicyFile().SetAndGet(context.Background(), in, tc.ETagParameter)
346+
assert.NoError(t, err)
347+
assert.Equal(t, http.MethodPost, server.Method)
348+
assert.Equal(t, "/api/v2/tailnet/example.com/acl", server.Path)
349+
assert.Equal(t, tc.RequestIfMatchHeaderValue, server.Header.Get("If-Match"))
350+
assert.EqualValues(t, "application/json", server.Header.Get("Content-Type"))
351+
assert.EqualValues(t, &in, out)
352+
353+
var actualACL ACL
354+
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualACL))
355+
in.ETag = ""
356+
assert.EqualValues(t, in, actualACL)
357+
})
358+
}
340359
}
341360

342361
func TestClient_SetACL_HuJSON(t *testing.T) {
@@ -354,28 +373,47 @@ func TestClient_SetACL_HuJSON(t *testing.T) {
354373
}
355374

356375
func TestClient_SetACLWithETag(t *testing.T) {
357-
t.Parallel()
358-
359-
client, server := NewTestHarness(t)
360-
server.ResponseCode = http.StatusOK
361-
expectedACL := ACL{
362-
ACLs: []ACLEntry{
363-
{
364-
Action: "accept",
365-
Ports: []string{"*:*"},
366-
Users: []string{"*"},
367-
},
376+
testCases := []struct {
377+
Name string
378+
ETagParameter string
379+
RequestIfMatchHeaderValue string
380+
}{
381+
{
382+
Name: "ETag method parameter not wrapped in double quotes",
383+
ETagParameter: "test-ETag",
384+
RequestIfMatchHeaderValue: `"test-ETag"`,
385+
},
386+
{
387+
Name: "ETag method parameter wrapped in double quotes",
388+
ETagParameter: `"test-ETag"`,
389+
RequestIfMatchHeaderValue: `"test-ETag"`,
368390
},
369391
}
370392

371-
assert.NoError(t, client.PolicyFile().Set(context.Background(), expectedACL, "test-etag"))
372-
assert.Equal(t, http.MethodPost, server.Method)
373-
assert.Equal(t, "/api/v2/tailnet/example.com/acl", server.Path)
374-
assert.Equal(t, `"test-etag"`, server.Header.Get("If-Match"))
393+
for _, tc := range testCases {
394+
t.Run(tc.Name, func(t *testing.T) {
395+
client, server := NewTestHarness(t)
396+
server.ResponseCode = http.StatusOK
397+
expectedACL := ACL{
398+
ACLs: []ACLEntry{
399+
{
400+
Action: "accept",
401+
Ports: []string{"*:*"},
402+
Users: []string{"*"},
403+
},
404+
},
405+
}
375406

376-
var actualACL ACL
377-
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualACL))
378-
assert.EqualValues(t, expectedACL, actualACL)
407+
assert.NoError(t, client.PolicyFile().Set(context.Background(), expectedACL, tc.ETagParameter))
408+
assert.Equal(t, http.MethodPost, server.Method)
409+
assert.Equal(t, "/api/v2/tailnet/example.com/acl", server.Path)
410+
assert.Equal(t, tc.RequestIfMatchHeaderValue, server.Header.Get("If-Match"))
411+
412+
var actualACL ACL
413+
assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualACL))
414+
assert.EqualValues(t, expectedACL, actualACL)
415+
})
416+
}
379417
}
380418

381419
func TestClient_ACL(t *testing.T) {

0 commit comments

Comments
 (0)