Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions acls.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ type ACLsService struct{ client *Client }
// "clients", "groups", "containers", "cookbooks", "data", "environments",
// "policies", "policy_groups", "roles", ...).
func (s *ACLsService) Get(ctx context.Context, objectType, name string) (*ACL, *Response, error) {
return s.getACL(ctx, s.client.orgPath(objectType+"/"+name))
return s.getACL(ctx, s.client.orgPath(esc(objectType)+"/"+esc(name)))
}

// SetPermission rewrites one permission's ACE on one object. The Chef API
Expand All @@ -145,13 +145,13 @@ func (s *ACLsService) Get(ctx context.Context, objectType, name string) (*ACL, *
// Nil Actors/Groups slices are coerced to empty arrays so the server does
// not reject the request for a null member list.
func (s *ACLsService) SetPermission(ctx context.Context, objectType, name, perm string, ace *ACE) error {
return s.setACL(ctx, s.client.orgPath(objectType+"/"+name), perm, ace)
return s.setACL(ctx, s.client.orgPath(esc(objectType)+"/"+esc(name)), perm, ace)
}

// GetOrg returns the ACL of the organization object itself, served at
// /organizations/ORG/_acl (no object-type segment).
func (s *ACLsService) GetOrg(ctx context.Context) (*ACL, *Response, error) {
return s.getACL(ctx, "/organizations/"+s.client.org)
return s.getACL(ctx, "/organizations/"+esc(s.client.org))
}

// SetOrgPermission rewrites one permission's ACE on the organization object.
Expand All @@ -162,12 +162,12 @@ func (s *ACLsService) SetOrgPermission(ctx context.Context, perm string, ace *AC
// GetUser returns the ACL of a global user object. User ACLs are top-level
// (/users/USER/_acl), not org-scoped.
func (s *ACLsService) GetUser(ctx context.Context, name string) (*ACL, *Response, error) {
return s.getACL(ctx, "/users/"+name)
return s.getACL(ctx, "/users/"+esc(name))
}

// SetUserPermission rewrites one permission's ACE on a global user object.
func (s *ACLsService) SetUserPermission(ctx context.Context, name, perm string, ace *ACE) error {
return s.setACL(ctx, "/users/"+name, perm, ace)
return s.setACL(ctx, "/users/"+esc(name), perm, ace)
}

// getACL fetches the full ACL for the object whose path is base (without the
Expand All @@ -188,6 +188,6 @@ func (s *ACLsService) setACL(ctx context.Context, base, perm string, ace *ACE) e
"groups": nonNil(ace.Groups),
},
}
_, _, err := do[map[string]any](ctx, s.client, "PUT", base+"/_acl/"+perm, body)
_, _, err := do[map[string]any](ctx, s.client, "PUT", base+"/_acl/"+esc(perm), body)
return err
}
14 changes: 7 additions & 7 deletions associations.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (s *AssociationsService) ListMembers(ctx context.Context) ([]string, *Respo

// GetMember returns one organization member's record.
func (s *AssociationsService) GetMember(ctx context.Context, name string) (*OrgUser, *Response, error) {
u, resp, err := do[OrgUser](ctx, s.client, "GET", s.client.orgPath("/users/"+name), nil)
u, resp, err := do[OrgUser](ctx, s.client, "GET", s.client.orgPath("/users/"+esc(name)), nil)
return ptrOrNil(u, err), resp, err
}

Expand All @@ -70,7 +70,7 @@ func (s *AssociationsService) AddMember(ctx context.Context, username string) (*
// RemoveMember removes a user's association with the organization and returns
// the user's end state.
func (s *AssociationsService) RemoveMember(ctx context.Context, name string) (*OrgUser, *Response, error) {
u, resp, err := do[OrgUser](ctx, s.client, "DELETE", s.client.orgPath("/users/"+name), nil)
u, resp, err := do[OrgUser](ctx, s.client, "DELETE", s.client.orgPath("/users/"+esc(name)), nil)
return ptrOrNil(u, err), resp, err
}

Expand All @@ -89,22 +89,22 @@ func (s *AssociationsService) Invite(ctx context.Context, username string) (*Inv
// RescindInvite cancels a pending organization invitation by its ID.
func (s *AssociationsService) RescindInvite(ctx context.Context, id string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/association_requests/"+id), nil)
s.client.orgPath("/association_requests/"+esc(id)), nil)
return resp, err
}

// ListUserInvites returns the invitations pending for the named global user.
// This is the user-side view at /users/USER/association_requests, so the
// invitations carry OrgName rather than Username.
func (s *AssociationsService) ListUserInvites(ctx context.Context, username string) ([]Invitation, *Response, error) {
return do[[]Invitation](ctx, s.client, "GET", "/users/"+username+"/association_requests", nil)
return do[[]Invitation](ctx, s.client, "GET", "/users/"+esc(username)+"/association_requests", nil)
}

// UserInviteCount returns the number of invitations pending for the user.
func (s *AssociationsService) UserInviteCount(ctx context.Context, username string) (int, *Response, error) {
v, resp, err := do[struct {
Value int `json:"value"`
}](ctx, s.client, "GET", "/users/"+username+"/association_requests/count", nil)
}](ctx, s.client, "GET", "/users/"+esc(username)+"/association_requests/count", nil)
return v.Value, resp, err
}

Expand All @@ -116,7 +116,7 @@ func (s *AssociationsService) RespondInvite(ctx context.Context, username, id st
response = "accept"
}
_, resp, err := do[map[string]any](ctx, s.client, "PUT",
"/users/"+username+"/association_requests/"+id, map[string]string{"response": response})
"/users/"+esc(username)+"/association_requests/"+esc(id), map[string]string{"response": response})
return resp, err
}

Expand All @@ -127,7 +127,7 @@ type userOrg struct {

// ListUserOrgs returns the organizations the named global user belongs to.
func (s *AssociationsService) ListUserOrgs(ctx context.Context, username string) ([]Org, *Response, error) {
wrapped, resp, err := do[[]userOrg](ctx, s.client, "GET", "/users/"+username+"/organizations", nil)
wrapped, resp, err := do[[]userOrg](ctx, s.client, "GET", "/users/"+esc(username)+"/organizations", nil)
if err != nil {
return nil, resp, err
}
Expand Down
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func cloneTransportSkipVerify(base http.RoundTripper) *http.Transport {

// orgPath prefixes p with /organizations/<org>.
func (c *Client) orgPath(p string) string {
return "/organizations/" + c.org + "/" + strings.TrimLeft(p, "/")
return "/organizations/" + esc(c.org) + "/" + strings.TrimLeft(p, "/")
}

// sleepCtx waits for d, reporting false if ctx ended first.
Expand Down
4 changes: 2 additions & 2 deletions containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (s *ContainersService) List(ctx context.Context) (map[string]string, *Respo
// Get retrieves a single container by name.
func (s *ContainersService) Get(ctx context.Context, name string) (*Container, *Response, error) {
cn, resp, err := do[Container](ctx, s.client, "GET",
s.client.orgPath("/containers/"+name), nil)
s.client.orgPath("/containers/"+esc(name)), nil)
return ptrOrNil(cn, err), resp, err
}

Expand All @@ -39,6 +39,6 @@ func (s *ContainersService) Create(ctx context.Context, name string) (*Response,
// Delete removes a container by name.
func (s *ContainersService) Delete(ctx context.Context, name string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/containers/"+name), nil)
s.client.orgPath("/containers/"+esc(name)), nil)
return resp, err
}
6 changes: 3 additions & 3 deletions cookbook_artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (s *CookbookArtifactsService) List(ctx context.Context) (map[string]Cookboo
// {name: {url, versions}} envelope.
func (s *CookbookArtifactsService) GetVersions(ctx context.Context, name string) (*CookbookArtifactListEntry, *Response, error) {
m, resp, err := do[map[string]CookbookArtifactListEntry](ctx, s.client, "GET",
s.client.orgPath("/cookbook_artifacts/"+name), nil)
s.client.orgPath("/cookbook_artifacts/"+esc(name)), nil)
if err != nil {
return nil, resp, err
}
Expand All @@ -46,14 +46,14 @@ func (s *CookbookArtifactsService) GetVersions(ctx context.Context, name string)
// Get retrieves a single cookbook artifact by name and identifier.
func (s *CookbookArtifactsService) Get(ctx context.Context, name, identifier string) (*Cookbook, *Response, error) {
cb, resp, err := do[Cookbook](ctx, s.client, "GET",
s.client.orgPath("/cookbook_artifacts/"+name+"/"+identifier), nil)
s.client.orgPath("/cookbook_artifacts/"+esc(name)+"/"+esc(identifier)), nil)
return ptrOrNil(cb, err), resp, err
}

// Delete removes a single cookbook artifact.
func (s *CookbookArtifactsService) Delete(ctx context.Context, name, identifier string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/cookbook_artifacts/"+name+"/"+identifier), nil)
s.client.orgPath("/cookbook_artifacts/"+esc(name)+"/"+esc(identifier)), nil)
return resp, err
}

Expand Down
8 changes: 4 additions & 4 deletions cookbooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (s *CookbooksService) ListRecipes(ctx context.Context) ([]string, *Response
// ("" for the server default of one, "all" for every version, or "n");
// versions come back newest-first.
func (s *CookbooksService) GetVersions(ctx context.Context, name, numVersions string) (*CookbookListEntry, *Response, error) {
path := s.client.orgPath("/cookbooks/" + name)
path := s.client.orgPath("/cookbooks/" + esc(name))
if numVersions != "" {
path += "?num_versions=" + url.QueryEscape(numVersions)
}
Expand All @@ -183,14 +183,14 @@ func (s *CookbooksService) GetVersions(ctx context.Context, name, numVersions st
// Get retrieves a single cookbook version manifest.
func (s *CookbooksService) Get(ctx context.Context, name, version string) (*Cookbook, *Response, error) {
cb, resp, err := do[Cookbook](ctx, s.client, "GET",
s.client.orgPath("/cookbooks/"+name+"/"+version), nil)
s.client.orgPath("/cookbooks/"+esc(name)+"/"+esc(version)), nil)
return ptrOrNil(cb, err), resp, err
}

// Delete removes a single cookbook version.
func (s *CookbooksService) Delete(ctx context.Context, name, version string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/cookbooks/"+name+"/"+version), nil)
s.client.orgPath("/cookbooks/"+esc(name)+"/"+esc(version)), nil)
return resp, err
}

Expand Down Expand Up @@ -304,7 +304,7 @@ func uploadCookbook(ctx context.Context, c *Client, base string, cb *LocalCookbo
slug = cb.Identifier
}
_, _, err = do[map[string]any](ctx, c, "PUT",
c.orgPath(base+"/"+cb.Name+"/"+slug), manifest)
c.orgPath(base+"/"+esc(cb.Name)+"/"+esc(slug)), manifest)
if err != nil {
return fmt.Errorf("cinc: put cookbook manifest: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type crud[T any] struct {
path string // resource collection path, e.g. "/nodes"
}

func (r crud[T]) item(name string) string { return r.client.orgPath(r.path + "/" + name) }
func (r crud[T]) item(name string) string { return r.client.orgPath(r.path + "/" + esc(name)) }
func (r crud[T]) coll() string { return r.client.orgPath(r.path) }

func (r crud[T]) get(ctx context.Context, name string) (T, *Response, error) {
Expand Down
6 changes: 3 additions & 3 deletions databags.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (s *DataBagsService) Create(ctx context.Context, name string) (*Response, e
// Delete removes a data bag and all its items.
func (s *DataBagsService) Delete(ctx context.Context, name string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/data/"+name), nil)
s.client.orgPath("/data/"+esc(name)), nil)
return resp, err
}

Expand All @@ -48,9 +48,9 @@ type DataBagItemsService struct {
bag string
}

func (s *DataBagItemsService) coll() string { return s.client.orgPath("/data/" + s.bag) }
func (s *DataBagItemsService) coll() string { return s.client.orgPath("/data/" + esc(s.bag)) }
func (s *DataBagItemsService) item(id string) string {
return s.client.orgPath("/data/" + s.bag + "/" + id)
return s.client.orgPath("/data/" + esc(s.bag) + "/" + esc(id))
}

// List returns the item id->URL index for the bag.
Expand Down
12 changes: 6 additions & 6 deletions environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (s *EnvironmentsService) List(ctx context.Context) (map[string]string, *Res
// envCookbookQuery builds an environment cookbook path, appending the optional
// num_versions query parameter when non-empty.
func (s *EnvironmentsService) envPath(env, suffix, numVersions string) string {
p := s.client.orgPath("/environments/" + env + suffix)
p := s.client.orgPath("/environments/" + esc(env) + suffix)
if numVersions != "" {
p += "?num_versions=" + url.QueryEscape(numVersions)
}
Expand All @@ -71,36 +71,36 @@ func (s *EnvironmentsService) ListCookbooks(ctx context.Context, env, numVersion
// environment, filtered by the environment's version constraints.
func (s *EnvironmentsService) GetCookbook(ctx context.Context, env, name, numVersions string) (map[string]CookbookListEntry, *Response, error) {
return do[map[string]CookbookListEntry](ctx, s.client, "GET",
s.envPath(env, "/cookbooks/"+name, numVersions), nil)
s.envPath(env, "/cookbooks/"+esc(name), numVersions), nil)
}

// CookbookVersions solves the given run list against the environment and
// returns the cookbook versions (including dependencies) required to satisfy
// it, keyed by cookbook name.
func (s *EnvironmentsService) CookbookVersions(ctx context.Context, env string, runList []string) (map[string]Cookbook, *Response, error) {
return do[map[string]Cookbook](ctx, s.client, "POST",
s.client.orgPath("/environments/"+env+"/cookbook_versions"),
s.client.orgPath("/environments/"+esc(env)+"/cookbook_versions"),
map[string][]string{"run_list": runList})
}

// ListNodes returns the name->URL index of nodes in the environment.
func (s *EnvironmentsService) ListNodes(ctx context.Context, env string) (map[string]string, *Response, error) {
return do[map[string]string](ctx, s.client, "GET",
s.client.orgPath("/environments/"+env+"/nodes"), nil)
s.client.orgPath("/environments/"+esc(env)+"/nodes"), nil)
}

// ListRecipes returns the recipes available to the environment.
func (s *EnvironmentsService) ListRecipes(ctx context.Context, env string) ([]string, *Response, error) {
return do[[]string](ctx, s.client, "GET",
s.client.orgPath("/environments/"+env+"/recipes"), nil)
s.client.orgPath("/environments/"+esc(env)+"/recipes"), nil)
}

// RoleRunList returns the role's run list as scoped to the environment: the
// role's env_run_lists[env], or its default run_list for the _default
// environment.
func (s *EnvironmentsService) RoleRunList(ctx context.Context, env, role string) ([]string, *Response, error) {
rl, resp, err := do[runListBody](ctx, s.client, "GET",
s.client.orgPath("/environments/"+env+"/roles/"+role), nil)
s.client.orgPath("/environments/"+esc(env)+"/roles/"+esc(role)), nil)
return rl.RunList, resp, err
}

Expand Down
6 changes: 3 additions & 3 deletions groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (s *GroupsService) List(ctx context.Context) (map[string]string, *Response,
// Get retrieves a single group by name, including its members.
func (s *GroupsService) Get(ctx context.Context, name string) (*Group, *Response, error) {
g, resp, err := do[Group](ctx, s.client, "GET",
s.client.orgPath("/groups/"+name), nil)
s.client.orgPath("/groups/"+esc(name)), nil)
return ptrOrNil(g, err), resp, err
}

Expand Down Expand Up @@ -62,7 +62,7 @@ func (s *GroupsService) Update(ctx context.Context, g *Group) (*Group, *Response
},
}
updated, resp, err := do[Group](ctx, s.client, "PUT",
s.client.orgPath("/groups/"+name), body)
s.client.orgPath("/groups/"+esc(name)), body)
return ptrOrNil(updated, err), resp, err
}

Expand All @@ -79,7 +79,7 @@ func (g *Group) name() string {
// Delete removes a group by name.
func (s *GroupsService) Delete(ctx context.Context, name string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE",
s.client.orgPath("/groups/"+name), nil)
s.client.orgPath("/groups/"+esc(name)), nil)
return resp, err
}

Expand Down
6 changes: 3 additions & 3 deletions keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ type KeysService struct{ client *Client }

// User returns a handle to the keys of the named global user.
func (s *KeysService) User(name string) *KeyScope {
return &KeyScope{client: s.client, path: "/users/" + name + "/keys"}
return &KeyScope{client: s.client, path: "/users/" + esc(name) + "/keys"}
}

// Client returns a handle to the keys of the named org client.
func (s *KeysService) Client(name string) *KeyScope {
return &KeyScope{
client: s.client,
path: s.client.orgPath("/clients/" + name + "/keys"),
path: s.client.orgPath("/clients/" + esc(name) + "/keys"),
}
}

Expand All @@ -52,7 +52,7 @@ type KeyScope struct {
path string // absolute server path of the keys collection
}

func (s *KeyScope) item(name string) string { return s.path + "/" + name }
func (s *KeyScope) item(name string) string { return s.path + "/" + esc(name) }

// List returns every key in the scope.
func (s *KeyScope) List(ctx context.Context) ([]Key, *Response, error) {
Expand Down
6 changes: 3 additions & 3 deletions orgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (s *OrgsService) List(ctx context.Context) (map[string]string, *Response, e

// Get retrieves one organization's metadata.
func (s *OrgsService) Get(ctx context.Context, name string) (*Org, *Response, error) {
o, resp, err := do[Org](ctx, s.client, "GET", "/organizations/"+name, nil)
o, resp, err := do[Org](ctx, s.client, "GET", "/organizations/"+esc(name), nil)
return ptrOrNil(o, err), resp, err
}

Expand All @@ -44,12 +44,12 @@ func (s *OrgsService) Create(ctx context.Context, o *Org) (*OrgCreateResult, *Re

// Update replaces an organization's metadata (typically FullName).
func (s *OrgsService) Update(ctx context.Context, o *Org) (*Org, *Response, error) {
updated, resp, err := do[Org](ctx, s.client, "PUT", "/organizations/"+o.Name, o)
updated, resp, err := do[Org](ctx, s.client, "PUT", "/organizations/"+esc(o.Name), o)
return ptrOrNil(updated, err), resp, err
}

// Delete removes an organization.
func (s *OrgsService) Delete(ctx context.Context, name string) (*Response, error) {
_, resp, err := do[map[string]any](ctx, s.client, "DELETE", "/organizations/"+name, nil)
_, resp, err := do[map[string]any](ctx, s.client, "DELETE", "/organizations/"+esc(name), nil)
return resp, err
}
18 changes: 18 additions & 0 deletions pathescape.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cinc

import "net/url"

// esc percent-encodes a caller-supplied identifier so it occupies exactly one
// path segment.
//
// Two things depend on this. The v1.3 signature covers the canonical request
// path, and net/http re-derives the wire path from the parsed URL — so an
// unescaped name that Go encodes differently (a space, a non-ASCII rune) is
// signed one way and sent another, and the server rejects it with a 401. And a
// name containing "/" or ".." would otherwise walk out of the collection its
// service owns, producing a correctly-signed request against a different
// object entirely.
//
// Every identifier Chef itself considers legal is unreserved, so for valid
// input this is the identity function and nothing on the wire changes.
func esc(s string) string { return url.PathEscape(s) }
Loading