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

Commit1db9559

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 parent5e55406 commit1db9559

File tree

5 files changed

+110
-39
lines changed

5 files changed

+110
-39
lines changed

‎coderd/apikey_scopes_validation_test.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/coder/coder/v2/coderd/coderdtest"
1111
"github.com/coder/coder/v2/codersdk"
1212
"github.com/coder/coder/v2/testutil"
13-
"github.com/coder/coder/v2/x/wildcard"
1413
)
1514

1615
funcTestTokenCreation_ScopeValidation(t*testing.T) {
@@ -94,9 +93,10 @@ func TestTokenCreation_AllowListValidation(t *testing.T) {
9493
// Invalid resource type should be rejected.
9594
_,err:=client.CreateToken(ctx,codersdk.Me, codersdk.CreateTokenRequest{
9695
Scopes: []codersdk.APIKeyScope{codersdk.APIKeyScopeWorkspaceRead},
97-
AllowList: []codersdk.APIAllowListTarget{
98-
{Type:wildcard.Of(codersdk.RBACResource("unknown")),ID:wildcard.Of(uuid.New())},
99-
},
96+
AllowList: []codersdk.APIAllowListTarget{{
97+
Type:codersdk.RBACResource("unknown"),
98+
ID:uuid.New().String(),
99+
}},
100100
})
101101
require.Error(t,err)
102102

‎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: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
"github.com/coder/coder/v2/coderd/rbac/policy"
1111
)
1212

13-
// APIAllowListTarget represents a single allow-list entry using the canonical
14-
// string form "<resource_type>:<id>". The wildcard symbol "*" is treated as a
15-
// permissive match for either side.
13+
// APIAllowListTarget represents a single allow-list entry. The canonical string
14+
// form is "<resource_type>:<id>" with "*" acting as a wildcard for either
15+
// component. Structured JSON callers should use the object form
16+
// `{ "type": "workspace", "id": "<uuid>" }`.
1617
typeAPIAllowListTargetstruct {
1718
TypeRBACResource`json:"type"`
1819
IDstring`json:"id"`
@@ -30,51 +31,87 @@ func AllowResourceTarget(r RBACResource, id uuid.UUID) APIAllowListTarget {
3031
returnAPIAllowListTarget{Type:r,ID:id.String()}
3132
}
3233

33-
// String returns the canonical string representation "<type>:<id>" with"*"wildcards.
34+
// String returns the canonical string representation "<type>:<id>" with wildcards preserved.
3435
func (tAPIAllowListTarget)String()string {
3536
returnstring(t.Type)+":"+t.ID
3637
}
3738

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>".
39+
// UnmarshalJSON accepts either the structured object representation
40+
// `{ "type": "workspace", "id": "<uuid>" }` or the legacy string form "workspace:<uuid>".
4441
func (t*APIAllowListTarget)UnmarshalJSON(b []byte)error {
45-
varsstring
46-
iferr:=json.Unmarshal(b,&s);err!=nil {
47-
returnerr
42+
iflen(b)==0 {
43+
returnxerrors.New("empty allow_list entry")
44+
}
45+
46+
// Attempt to decode the structured object form first.
47+
varobjstruct {
48+
Typestring`json:"type"`
49+
IDstring`json:"id"`
4850
}
49-
parts:=strings.SplitN(strings.TrimSpace(s),":",2)
51+
iferr:=json.Unmarshal(b,&obj);err==nil {
52+
ifobj.Type==""&&obj.ID=="" {
53+
// Fall through to legacy string handling if both are empty and no error occurred.
54+
}elseifobj.Type==""||obj.ID=="" {
55+
returnxerrors.New("allow_list entry must include both type and id")
56+
}else {
57+
returnt.setValues(obj.Type,obj.ID)
58+
}
59+
}
60+
61+
varlegacystring
62+
iferr:=json.Unmarshal(b,&legacy);err!=nil {
63+
returnxerrors.New("invalid allow_list entry: expected object with type/id or string")
64+
}
65+
parts:=strings.SplitN(strings.TrimSpace(legacy),":",2)
5066
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
51-
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",s)
67+
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",legacy)
68+
}
69+
returnt.setValues(parts[0],parts[1])
70+
}
71+
72+
func (t*APIAllowListTarget)setValues(rawType,rawIDstring)error {
73+
rawType=strings.TrimSpace(rawType)
74+
rawID=strings.TrimSpace(rawID)
75+
76+
ifrawType==""||rawID=="" {
77+
returnxerrors.New("allow_list entry must include non-empty type and id")
5278
}
5379

54-
// Type
55-
resource:=RBACResource(parts[0])
56-
ifresource!=ResourceWildcard {
57-
if_,ok:=policy.RBACPermissions[parts[0]];!ok {
58-
returnxerrors.Errorf("unknown resource type %q",parts[0])
80+
ifrawType==policy.WildcardSymbol {
81+
t.Type=ResourceWildcard
82+
}else {
83+
if_,ok:=policy.RBACPermissions[rawType];!ok {
84+
returnxerrors.Errorf("unknown resource type %q",rawType)
5985
}
86+
t.Type=RBACResource(rawType)
6087
}
61-
t.Type=resource
6288

63-
// ID
64-
id:=parts[1]
65-
ifid!=policy.WildcardSymbol {
66-
if_,err:=uuid.Parse(id);err!=nil {
67-
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",parts[0],parts[1])
68-
}
89+
ifrawID==policy.WildcardSymbol {
90+
t.ID=policy.WildcardSymbol
91+
returnnil
92+
}
93+
94+
if_,err:=uuid.Parse(rawID);err!=nil {
95+
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",rawType,rawID)
6996
}
70-
t.ID=id
97+
t.ID=rawID
7198
returnnil
7299
}
73100

74-
// Implement encoding.TextMarshaler/Unmarshaler for broader compatibility
101+
// MarshalJSON ensures encoding/json uses the structured representation instead
102+
// of the legacy colon-delimited string form.
103+
func (tAPIAllowListTarget)MarshalJSON() ([]byte,error) {
104+
typealiasAPIAllowListTarget
105+
returnjson.Marshal(alias(t))
106+
}
75107

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

78110
func (t*APIAllowListTarget)UnmarshalText(b []byte)error {
79-
returnt.UnmarshalJSON([]byte("\""+string(b)+"\""))
111+
strTarget:=strings.TrimSpace(string(b))
112+
parts:=strings.SplitN(strTarget,":",2)
113+
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
114+
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",strTarget)
115+
}
116+
returnt.setValues(parts[0],parts[1])
80117
}

‎codersdk/allowlist_test.go‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
1717
all:=codersdk.AllowAllTarget()
1818
b,err:=json.Marshal(all)
1919
require.NoError(t,err)
20-
require.JSONEq(t,`"*:*"`,string(b))
20+
require.JSONEq(t,`{"type":"*","id":"*"}`,string(b))
2121
varrt codersdk.APIAllowListTarget
2222
require.NoError(t,json.Unmarshal(b,&rt))
2323
require.Equal(t,codersdk.ResourceWildcard,rt.Type)
@@ -26,7 +26,7 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
2626
ty:=codersdk.AllowTypeTarget(codersdk.ResourceWorkspace)
2727
b,err=json.Marshal(ty)
2828
require.NoError(t,err)
29-
require.JSONEq(t,`"workspace:*"`,string(b))
29+
require.JSONEq(t,`{"type":"workspace","id":"*"}`,string(b))
3030
require.NoError(t,json.Unmarshal(b,&rt))
3131
require.Equal(t,codersdk.ResourceWorkspace,rt.Type)
3232
require.Equal(t,policy.WildcardSymbol,rt.ID)
@@ -35,6 +35,24 @@ func TestAPIAllowListTarget_JSONRoundTrip(t *testing.T) {
3535
res:=codersdk.AllowResourceTarget(codersdk.ResourceTemplate,id)
3636
b,err=json.Marshal(res)
3737
require.NoError(t,err)
38-
exp:=`"template:`+id.String()+`"`
38+
exp:=`{"type":"template","id":"`+id.String()+`"}`
3939
require.JSONEq(t,exp,string(b))
4040
}
41+
42+
funcTestAPIAllowListTarget_UnmarshalText(t*testing.T) {
43+
t.Parallel()
44+
45+
vartarget codersdk.APIAllowListTarget
46+
require.NoError(t,target.UnmarshalText([]byte("workspace:123e4567-e89b-12d3-a456-426614174000")))
47+
require.Equal(t,codersdk.ResourceWorkspace,target.Type)
48+
require.Equal(t,"123e4567-e89b-12d3-a456-426614174000",target.ID)
49+
}
50+
51+
funcTestAPIAllowListTarget_UnmarshalObject(t*testing.T) {
52+
t.Parallel()
53+
54+
vartarget codersdk.APIAllowListTarget
55+
require.NoError(t,json.Unmarshal([]byte(`{"type":"workspace","id":"*"}`),&target))
56+
require.Equal(t,codersdk.ResourceWorkspace,target.Type)
57+
require.Equal(t,policy.WildcardSymbol,target.ID)
58+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp