Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfcd6e93

Browse files
committed
feat: add structured JSON format for APIAllowListTarget
APIAllowListTarget now marshals to/from structured JSON objects`{"type":"workspace","id":"<uuid>"}` instead of colon-delimitedstrings. This improves type safety and frontend ergonomics.Changes:- Modified UnmarshalJSON to parse structured object representation- Extracted setValues helper for shared validation logic- Preserved UnmarshalText for backward compatibility with CLI flags and database helpers- Added MarshalJSON/UnmarshalJSON to x/wildcard/Value for proper JSON handling of wildcard values- Updated frontend mock data to use structured format- Added test coverage for both text and object unmarshaling- Added resource ID matchers to regosql converters for template and workspace ID filtering
1 parent5058a5a commitfcd6e93

File tree

7 files changed

+128
-31
lines changed

7 files changed

+128
-31
lines changed

‎coderd/rbac/regosql/compile_test.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ func TestRegoQueries(t *testing.T) {
217217
" OR (workspaces.group_acl#>array['96c55a0e-73b4-44fc-abac-70d53c35c04c', 'permissions'] ? '*'))",
218218
VariableConverter:regosql.WorkspaceConverter(),
219219
},
220+
{
221+
Name:"WorkspaceIDMatcher",
222+
Queries: []string{
223+
`input.object.id = "a8d0f8ce-6a01-4d0d-ab1d-1d546958feae"`,
224+
},
225+
ExpectedSQL:p("workspaces.id :: text = 'a8d0f8ce-6a01-4d0d-ab1d-1d546958feae'"),
226+
VariableConverter:regosql.WorkspaceConverter(),
227+
},
220228
{
221229
Name:"NoACLConfig",
222230
Queries: []string{
@@ -262,6 +270,14 @@ neq(input.object.owner, "");
262270
p("false")),
263271
VariableConverter:regosql.TemplateConverter(),
264272
},
273+
{
274+
Name:"TemplateIDMatcher",
275+
Queries: []string{
276+
`input.object.id = "a829cb9d-7c5b-4c3b-bf78-053827a56e58"`,
277+
},
278+
ExpectedSQL:p("t.id :: text = 'a829cb9d-7c5b-4c3b-bf78-053827a56e58'"),
279+
VariableConverter:regosql.TemplateConverter(),
280+
},
265281
{
266282
Name:"UserNoOrgOwner",
267283
Queries: []string{

‎coderd/rbac/regosql/configs.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func userACLMatcher(m sqltypes.VariableMatcher) ACLMappingVar {
2424

2525
funcTemplateConverter()*sqltypes.VariableConverter {
2626
matcher:=sqltypes.NewVariableConverter().RegisterMatcher(
27-
resourceIDMatcher(),
27+
sqltypes.StringVarMatcher("t.id :: text", []string{"input","object","id"}),
2828
sqltypes.StringVarMatcher("t.organization_id :: text", []string{"input","object","org_owner"}),
2929
// Templates have no user owner, only owner by an organization.
3030
sqltypes.AlwaysFalse(userOwnerMatcher()),
@@ -38,7 +38,7 @@ func TemplateConverter() *sqltypes.VariableConverter {
3838

3939
funcWorkspaceConverter()*sqltypes.VariableConverter {
4040
matcher:=sqltypes.NewVariableConverter().RegisterMatcher(
41-
resourceIDMatcher(),
41+
sqltypes.StringVarMatcher("workspaces.id :: text", []string{"input","object","id"}),
4242
sqltypes.StringVarMatcher("workspaces.organization_id :: text", []string{"input","object","org_owner"}),
4343
userOwnerMatcher(),
4444
)

‎codersdk/allowlist.go‎

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
// APIAllowListTarget is a typed allow-list entry that marshals to a single string
1515
// "<resource_type>:<id>" where "*" is used as a wildcard for either side.
1616
typeAPIAllowListTargetstruct {
17-
Type wildcard.Value[RBACResource]
18-
ID wildcard.Value[uuid.UUID]
17+
Type wildcard.Value[RBACResource]`json:"type"`
18+
ID wildcard.Value[uuid.UUID]`json:"id"`
1919
}
2020

2121
funcAllowAllTarget()APIAllowListTarget {
@@ -35,42 +35,68 @@ func (t APIAllowListTarget) String() string {
3535
returnt.Type.String()+":"+t.ID.String()
3636
}
3737

38-
// MarshalJSON encodes as a JSON string: "<type>:<id>".
39-
func (tAPIAllowListTarget)MarshalJSON() ([]byte,error) {
40-
returnjson.Marshal(t.String())
41-
}
42-
43-
// UnmarshalJSON decodes from a JSON string: "<type>:<id>".
38+
// UnmarshalJSON accepts the structured object representation
39+
// `{ "type": "workspace", "id": "<uuid>" }`.
4440
func (t*APIAllowListTarget)UnmarshalJSON(b []byte)error {
45-
varsstring
46-
iferr:=json.Unmarshal(b,&s);err!=nil {
41+
iflen(b)==0 {
42+
returnxerrors.New("empty allow_list entry")
43+
}
44+
45+
varobjmap[string]string
46+
iferr:=json.Unmarshal(b,&obj);err!=nil {
4747
returnerr
4848
}
49-
parts:=strings.SplitN(strings.TrimSpace(s),":",2)
50-
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
51-
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",s)
49+
50+
typeVal,hasType:=obj["type"]
51+
idVal,hasID:=obj["id"]
52+
// XOR so that either both must be set or neither must
53+
ifhasType!=hasID {
54+
returnxerrors.New("allow_list entry must include both type and id")
55+
}
56+
returnt.setValues(typeVal,idVal)
57+
}
58+
59+
func (t*APIAllowListTarget)setValues(rawType,rawIDstring)error {
60+
rawType=strings.TrimSpace(rawType)
61+
rawID=strings.TrimSpace(rawID)
62+
63+
ifrawType==""||rawID=="" {
64+
returnxerrors.New("allow_list entry must include non-empty type and id")
5265
}
5366

54-
// Type
55-
ifparts[0]!=policy.WildcardSymbol {
56-
t.Type=wildcard.Of(RBACResource(parts[0]))
67+
ifrawType!=policy.WildcardSymbol {
68+
t.Type=wildcard.Of(RBACResource(rawType))
5769
}
5870

59-
// ID
60-
ifparts[1]!=policy.WildcardSymbol {
61-
u,err:=uuid.Parse(parts[1])
71+
ifrawID!=policy.WildcardSymbol {
72+
u,err:=uuid.Parse(rawID)
6273
iferr!=nil {
63-
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",parts[0],parts[1])
74+
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",rawType,rawID)
6475
}
6576
t.ID=wildcard.Of(u)
6677
}
78+
6779
returnnil
6880
}
6981

70-
// Implement encoding.TextMarshaler/Unmarshaler for broader compatibility
82+
// MarshalJSON ensures encoding/json uses the structured representation. If we
83+
// relied solely on MarshalText, the encoder would emit the "type:id"
84+
// string, but other callers (CLI flag parsing, database helpers) depend on
85+
// that string form, so we keep both implementations.
86+
func (tAPIAllowListTarget)MarshalJSON() ([]byte,error) {
87+
typealiasAPIAllowListTarget
88+
returnjson.Marshal(alias(t))
89+
}
7190

7291
func (tAPIAllowListTarget)MarshalText() ([]byte,error) {return []byte(t.String()),nil }
7392

7493
func (t*APIAllowListTarget)UnmarshalText(b []byte)error {
75-
returnt.UnmarshalJSON([]byte("\""+string(b)+"\""))
94+
strTarget:=strings.TrimSpace(string(b))
95+
96+
parts:=strings.SplitN(strTarget,":",2)
97+
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
98+
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",strTarget)
99+
}
100+
101+
returnt.setValues(parts[0],parts[1])
76102
}

‎codersdk/allowlist_test.go‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
1616
all:=codersdk.AllowAllTarget()
1717
b,err:=json.Marshal(all)
1818
require.NoError(t,err)
19-
require.JSONEq(t,`"*:*"`,string(b))
19+
require.JSONEq(t,`{"type":"*","id":"*"}`,string(b))
2020
varrt codersdk.APIAllowListTarget
2121
require.NoError(t,json.Unmarshal(b,&rt))
2222
require.True(t,rt.Type.IsAny())
@@ -25,7 +25,7 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
2525
ty:=codersdk.AllowTypeTarget(codersdk.ResourceWorkspace)
2626
b,err=json.Marshal(ty)
2727
require.NoError(t,err)
28-
require.JSONEq(t,`"workspace:*"`,string(b))
28+
require.JSONEq(t,`{"type":"workspace","id":"*"}`,string(b))
2929
require.NoError(t,json.Unmarshal(b,&rt))
3030
r,ok:=rt.Type.Value()
3131
require.True(t,ok)
@@ -36,6 +36,30 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
3636
res:=codersdk.AllowResourceTarget(codersdk.ResourceTemplate,id)
3737
b,err=json.Marshal(res)
3838
require.NoError(t,err)
39-
exp:=`"template:`+id.String()+`"`
39+
exp:=`{"type":"template","id":"`+id.String()+`"}`
4040
require.JSONEq(t,exp,string(b))
4141
}
42+
43+
funcTestAPIAllowListTarget_UnmarshalText(t*testing.T) {
44+
t.Parallel()
45+
46+
vartarget codersdk.APIAllowListTarget
47+
require.NoError(t,target.UnmarshalText([]byte("workspace:123e4567-e89b-12d3-a456-426614174000")))
48+
r,ok:=target.Type.Value()
49+
require.True(t,ok)
50+
require.Equal(t,codersdk.ResourceWorkspace,r)
51+
id,ok:=target.ID.Value()
52+
require.True(t,ok)
53+
require.Equal(t,"123e4567-e89b-12d3-a456-426614174000",id.String())
54+
}
55+
56+
funcTestAPIAllowListTarget_UnmarshalObject(t*testing.T) {
57+
t.Parallel()
58+
59+
vartarget codersdk.APIAllowListTarget
60+
require.NoError(t,json.Unmarshal([]byte(`{"type":"workspace","id":"*"}`),&target))
61+
r,ok:=target.Type.Value()
62+
require.True(t,ok)
63+
require.Equal(t,codersdk.ResourceWorkspace,r)
64+
require.True(t,target.ID.IsAny())
65+
}

‎site/src/api/typesGenerated.ts‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎site/src/testHelpers/entities.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const MockToken: TypesGen.APIKeyWithOwner = {
8585
login_type:"token",
8686
scope:"all",
8787
scopes:["coder:all"],
88-
allow_list:[{Type:"*",ID:"*"}],
88+
allow_list:[{type:"*",id:"*"}],
8989
lifetime_seconds:2592000,
9090
token_name:"token-one",
9191
username:"admin",
@@ -103,7 +103,7 @@ export const MockTokens: TypesGen.APIKeyWithOwner[] = [
103103
login_type:"token",
104104
scope:"all",
105105
scopes:["coder:all"],
106-
allow_list:[{Type:"*",ID:"*"}],
106+
allow_list:[{type:"*",id:"*"}],
107107
lifetime_seconds:2592000,
108108
token_name:"token-two",
109109
username:"admin",

‎x/wildcard/wildcard.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ package wildcard
2020
import (
2121
"database/sql"
2222
"encoding"
23+
"encoding/json"
2324
"fmt"
2425
"reflect"
26+
"strings"
2527

2628
"github.com/google/uuid"
2729
"golang.org/x/xerrors"
@@ -103,6 +105,35 @@ func (a *Value[T]) UnmarshalText(b []byte) error {
103105
returnxerrors.Errorf("match.Any: unsupported element type %T for UnmarshalText",zero)
104106
}
105107

108+
// MarshalJSON encodes the wildcard value as its canonical JSON representation.
109+
func (aValue[T])MarshalJSON() ([]byte,error) {
110+
ifa.set {
111+
returnjson.Marshal(a.v)
112+
}
113+
114+
returnjson.Marshal(a.String())
115+
}
116+
117+
// UnmarshalJSON accepts string or null values, delegating to UnmarshalText for parsing.
118+
func (a*Value[T])UnmarshalJSON(b []byte)error {
119+
iflen(b)==0 {
120+
*a=Any[T]()
121+
returnnil
122+
}
123+
124+
ifstrings.EqualFold(strings.TrimSpace(string(b)),"*") {
125+
*a=Any[T]()
126+
returnnil
127+
}
128+
129+
varsstring
130+
iferr:=json.Unmarshal(b,&s);err!=nil {
131+
returnxerrors.Errorf("wildcard: expected string or *: %w",err)
132+
}
133+
134+
returna.UnmarshalText([]byte(s))
135+
}
136+
106137
// Scan implements sql.Scanner; accepts nil, []byte, or string and delegates to UnmarshalText.
107138
func (a*Value[T])Scan(srcany)error {
108139
switchv:=src.(type) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp