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

Commitd0f3a09

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 parent009ef1c commitd0f3a09

File tree

5 files changed

+109
-37
lines changed

5 files changed

+109
-37
lines changed

‎coderd/apikey_scopes_validation_test.go‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,10 @@ func TestTokenCreation_AllowListValidation(t *testing.T) {
9393
// Invalid resource type should be rejected.
9494
_,err:=client.CreateToken(ctx,codersdk.Me, codersdk.CreateTokenRequest{
9595
Scopes: []codersdk.APIKeyScope{codersdk.APIKeyScopeWorkspaceRead},
96-
AllowList: []codersdk.APIAllowListTarget{
97-
{Type:codersdk.RBACResource("unknown"),ID:uuid.New().String()},
98-
},
96+
AllowList: []codersdk.APIAllowListTarget{{
97+
Type:codersdk.RBACResource("unknown"),
98+
ID:uuid.New().String(),
99+
}},
99100
})
100101
require.Error(t,err)
101102

‎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: 66 additions & 29 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+
ifobj.Type==""||obj.ID=="" {
54+
returnxerrors.New("allow_list entry must include both type and id")
55+
}
56+
returnt.setValues(obj.Type,obj.ID)
57+
}
58+
}
59+
60+
varlegacystring
61+
iferr:=json.Unmarshal(b,&legacy);err!=nil {
62+
returnxerrors.New("invalid allow_list entry: expected object with type/id or string")
63+
}
64+
parts:=strings.SplitN(strings.TrimSpace(legacy),":",2)
5065
iflen(parts)!=2||parts[0]==""||parts[1]=="" {
51-
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",s)
66+
returnxerrors.Errorf("invalid allow_list entry %q: want <type>:<id>",legacy)
5267
}
68+
returnt.setValues(parts[0],parts[1])
69+
}
5370

54-
resource,id:=RBACResource(parts[0]),parts[1]
71+
func (t*APIAllowListTarget)setValues(rawType,rawIDstring)error {
72+
rawType=strings.TrimSpace(rawType)
73+
rawID=strings.TrimSpace(rawID)
5574

56-
// Type
57-
ifresource!=ResourceWildcard {
58-
if_,ok:=policy.RBACPermissions[string(resource)];!ok {
59-
returnxerrors.Errorf("unknown resource type %q",resource)
60-
}
75+
ifrawType==""||rawID=="" {
76+
returnxerrors.New("allow_list entry must include non-empty type and id")
6177
}
62-
t.Type=resource
6378

64-
// ID
65-
ifid!=policy.WildcardSymbol {
66-
if_,err:=uuid.Parse(id);err!=nil {
67-
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",resource,id)
79+
ifrawType==policy.WildcardSymbol {
80+
t.Type=ResourceWildcard
81+
}else {
82+
if_,ok:=policy.RBACPermissions[rawType];!ok {
83+
returnxerrors.Errorf("unknown resource type %q",rawType)
6884
}
85+
t.Type=RBACResource(rawType)
86+
}
87+
88+
ifrawID==policy.WildcardSymbol {
89+
t.ID=policy.WildcardSymbol
90+
returnnil
91+
}
92+
93+
if_,err:=uuid.Parse(rawID);err!=nil {
94+
returnxerrors.Errorf("invalid %s ID (must be UUID): %q",rawType,rawID)
6995
}
70-
t.ID=id
96+
t.ID=rawID
7197
returnnil
7298
}
7399

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

107+
// Implement encoding.TextMarshaler/Unmarshaler for broader compatibility.
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