@@ -10,12 +10,16 @@ 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>" }`. Optionally, servers may attach a
17+ // DisplayName to provide a human-friendly label; clients must ignore the field
18+ // when submitting data.
1619type APIAllowListTarget struct {
17- Type RBACResource `json:"type"`
18- ID string `json:"id"`
20+ Type RBACResource `json:"type"`
21+ ID string `json:"id"`
22+ DisplayName string `json:"display_name,omitempty"`
1923}
2024
2125func AllowAllTarget () APIAllowListTarget {
@@ -30,51 +34,92 @@ func AllowResourceTarget(r RBACResource, id uuid.UUID) APIAllowListTarget {
3034 return APIAllowListTarget {Type : r , ID : id .String ()}
3135}
3236
33- // String returns the canonical string representation "<type>:<id>" with "*" wildcards.
37+ // String returns the canonical string representation "<type>:<id>" with wildcards preserved .
3438func (t APIAllowListTarget ) String () string {
3539 return string (t .Type ) + ":" + t .ID
3640}
3741
38- // MarshalJSON encodes as a JSON string: "<type>:<id>".
39- func (t APIAllowListTarget ) MarshalJSON () ([]byte , error ) {
40- return json .Marshal (t .String ())
41- }
42-
43- // UnmarshalJSON decodes from a JSON string: "<type>:<id>".
42+ // UnmarshalJSON accepts either the structured object representation
43+ // `{ "type": "workspace", "id": "<uuid>" }` or the legacy string form "workspace:<uuid>".
4444func (t * APIAllowListTarget ) UnmarshalJSON (b []byte ) error {
45- var s string
46- if err := json .Unmarshal (b , & s ); err != nil {
47- return err
45+ if len (b ) == 0 {
46+ return xerrors .New ("empty allow_list entry" )
47+ }
48+
49+ // Attempt to decode the structured object form first.
50+ var obj struct {
51+ Type string `json:"type"`
52+ ID string `json:"id"`
53+ DisplayName string `json:"display_name"`
4854 }
49- parts := strings .SplitN (strings .TrimSpace (s ), ":" , 2 )
55+ if err := json .Unmarshal (b , & obj ); err == nil {
56+ if obj .Type != "" || obj .ID != "" {
57+ if obj .Type == "" || obj .ID == "" {
58+ return xerrors .New ("allow_list entry must include both type and id" )
59+ }
60+ if err := t .setValues (obj .Type , obj .ID ); err != nil {
61+ return err
62+ }
63+ // Ignore object.DisplayName on input to keep backend validation strict.
64+ return nil
65+ }
66+ }
67+
68+ var legacy string
69+ if err := json .Unmarshal (b , & legacy ); err != nil {
70+ return xerrors .New ("invalid allow_list entry: expected object with type/id or string" )
71+ }
72+ parts := strings .SplitN (strings .TrimSpace (legacy ), ":" , 2 )
5073 if len (parts ) != 2 || parts [0 ] == "" || parts [1 ] == "" {
51- return xerrors .Errorf ("invalid allow_list entry %q: want <type>:<id>" , s )
74+ return xerrors .Errorf ("invalid allow_list entry %q: want <type>:<id>" , legacy )
5275 }
76+ return t .setValues (parts [0 ], parts [1 ])
77+ }
5378
54- resource , id := RBACResource (parts [0 ]), parts [1 ]
79+ func (t * APIAllowListTarget ) setValues (rawType , rawID string ) error {
80+ rawType = strings .TrimSpace (rawType )
81+ rawID = strings .TrimSpace (rawID )
5582
56- // Type
57- if resource != ResourceWildcard {
58- if _ , ok := policy .RBACPermissions [string (resource )]; ! ok {
59- return xerrors .Errorf ("unknown resource type %q" , resource )
60- }
83+ if rawType == "" || rawID == "" {
84+ return xerrors .New ("allow_list entry must include non-empty type and id" )
6185 }
62- t .Type = resource
6386
64- // ID
65- if id != policy .WildcardSymbol {
66- if _ , err := uuid .Parse (id ); err != nil {
67- return xerrors .Errorf ("invalid %s ID (must be UUID): %q" , resource , id )
87+ if rawType == policy .WildcardSymbol {
88+ t .Type = ResourceWildcard
89+ } else {
90+ if _ , ok := policy .RBACPermissions [rawType ]; ! ok {
91+ return xerrors .Errorf ("unknown resource type %q" , rawType )
6892 }
93+ t .Type = RBACResource (rawType )
94+ }
95+
96+ if rawID == policy .WildcardSymbol {
97+ t .ID = policy .WildcardSymbol
98+ return nil
99+ }
100+
101+ if _ , err := uuid .Parse (rawID ); err != nil {
102+ return xerrors .Errorf ("invalid %s ID (must be UUID): %q" , rawType , rawID )
69103 }
70- t .ID = id
104+ t .ID = rawID
71105 return nil
72106}
73107
74- // Implement encoding.TextMarshaler/Unmarshaler for broader compatibility
108+ // MarshalJSON ensures encoding/json uses the structured representation instead
109+ // of the legacy colon-delimited string form.
110+ func (t APIAllowListTarget ) MarshalJSON () ([]byte , error ) {
111+ type alias APIAllowListTarget
112+ return json .Marshal (alias (t ))
113+ }
75114
115+ // Implement encoding.TextMarshaler/Unmarshaler for broader compatibility.
76116func (t APIAllowListTarget ) MarshalText () ([]byte , error ) { return []byte (t .String ()), nil }
77117
78118func (t * APIAllowListTarget ) UnmarshalText (b []byte ) error {
79- return t .UnmarshalJSON ([]byte ("\" " + string (b ) + "\" " ))
119+ strTarget := strings .TrimSpace (string (b ))
120+ parts := strings .SplitN (strTarget , ":" , 2 )
121+ if len (parts ) != 2 || parts [0 ] == "" || parts [1 ] == "" {
122+ return xerrors .Errorf ("invalid allow_list entry %q: want <type>:<id>" , strTarget )
123+ }
124+ return t .setValues (parts [0 ], parts [1 ])
80125}
0 commit comments