mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add shared_with_group: and shared_with_user: filters to /workspaces endpoint (#19875)
Adds shared_with_user and shared_with_group filters to the /workspaces endpoint. - `shared_with_user`: filters workspaces shared with a specific user. Accepts a user UUID or username. - `shared_with_group`: filters workspaces shared with a specific group. Accepts: - a group UUID, or - `<organization name>/<group name>`, or - `<group name>` (resolved in the default organization). Closes [coder/internal#1004](https://github.com/coder/internal/issues/1004)
This commit is contained in:
@@ -226,6 +226,8 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
|
||||
filter.HasExternalAgent = parser.NullableBoolean(values, sql.NullBool{}, "has_external_agent")
|
||||
filter.OrganizationID = parseOrganization(ctx, db, parser, values, "organization")
|
||||
filter.Shared = parser.NullableBoolean(values, sql.NullBool{}, "shared")
|
||||
filter.SharedWithUserID = parseUser(ctx, db, parser, values, "shared_with_user")
|
||||
filter.SharedWithGroupID = parseGroup(ctx, db, parser, values, "shared_with_group")
|
||||
|
||||
type paramMatch struct {
|
||||
name string
|
||||
@@ -363,6 +365,85 @@ func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.Q
|
||||
})
|
||||
}
|
||||
|
||||
func parseUser(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string) uuid.UUID {
|
||||
return httpapi.ParseCustom(parser, vals, uuid.Nil, queryParam, func(v string) (uuid.UUID, error) {
|
||||
if v == "" {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
userID, err := uuid.Parse(v)
|
||||
if err == nil {
|
||||
return userID, nil
|
||||
}
|
||||
user, err := db.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
|
||||
Username: v,
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, xerrors.Errorf("user %q either does not exist, or you are unauthorized to view them", v)
|
||||
}
|
||||
return user.ID, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Parse a group filter value into a group UUID.
|
||||
// Supported formats:
|
||||
// - <group-uuid>
|
||||
// - <organization-name>/<group-name>
|
||||
// - <group-name> (resolved in the default organization)
|
||||
func parseGroup(ctx context.Context, db database.Store, parser *httpapi.QueryParamParser, vals url.Values, queryParam string) uuid.UUID {
|
||||
return httpapi.ParseCustom(parser, vals, uuid.Nil, queryParam, func(v string) (uuid.UUID, error) {
|
||||
if v == "" {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
groupID, err := uuid.Parse(v)
|
||||
if err == nil {
|
||||
return groupID, nil
|
||||
}
|
||||
|
||||
var groupName string
|
||||
var org database.Organization
|
||||
parts := strings.Split(v, "/")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
dbOrg, err := db.GetDefaultOrganization(ctx)
|
||||
if err != nil {
|
||||
return uuid.Nil, xerrors.New("fetching default organization")
|
||||
}
|
||||
org = dbOrg
|
||||
groupName = parts[0]
|
||||
case 2:
|
||||
orgName := parts[0]
|
||||
if err := codersdk.NameValid(orgName); err != nil {
|
||||
return uuid.Nil, xerrors.Errorf("invalid organization name %w", err)
|
||||
}
|
||||
dbOrg, err := db.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{
|
||||
Name: orgName,
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, xerrors.Errorf("organization %q either does not exist, or you are unauthorized to view it", orgName)
|
||||
}
|
||||
org = dbOrg
|
||||
|
||||
groupName = parts[1]
|
||||
|
||||
default:
|
||||
return uuid.Nil, xerrors.New("invalid organization or group name, the filter must be in the pattern of <organization name>/<group name>")
|
||||
}
|
||||
|
||||
if err := codersdk.GroupNameValid(groupName); err != nil {
|
||||
return uuid.Nil, xerrors.Errorf("invalid group name %w", err)
|
||||
}
|
||||
|
||||
group, err := db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
|
||||
OrganizationID: org.ID,
|
||||
Name: groupName,
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, xerrors.Errorf("group %q either does not exist, does not belong to the organization %q, or you are unauthorized to view it", groupName, org.Name)
|
||||
}
|
||||
return group.ID, nil
|
||||
})
|
||||
}
|
||||
|
||||
// splitQueryParameterByDelimiter takes a query string and splits it into the individual elements
|
||||
// of the query. Each element is separated by a delimiter. All quoted strings are
|
||||
// kept as a single element.
|
||||
|
||||
@@ -312,6 +312,84 @@ func TestSearchWorkspace(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SharedWithUser",
|
||||
Query: `shared_with_user:3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf`,
|
||||
Setup: func(t *testing.T, db database.Store) {
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
|
||||
})
|
||||
},
|
||||
Expected: database.GetWorkspacesParams{
|
||||
SharedWithUserID: uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SharedWithUserByName",
|
||||
Query: `shared_with_user:wibble`,
|
||||
Setup: func(t *testing.T, db database.Store) {
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
|
||||
Username: "wibble",
|
||||
})
|
||||
},
|
||||
Expected: database.GetWorkspacesParams{
|
||||
SharedWithUserID: uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupDefaultOrg",
|
||||
Query: "shared_with_group:wibble",
|
||||
Setup: func(t *testing.T, db database.Store) {
|
||||
org, err := db.GetOrganizationByName(t.Context(), database.GetOrganizationByNameParams{
|
||||
Name: "coder",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
dbgen.Group(t, db, database.Group{
|
||||
ID: uuid.MustParse("590f1006-15e6-4b21-a6e1-92e33af8a5c3"),
|
||||
Name: "wibble",
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
},
|
||||
Expected: database.GetWorkspacesParams{
|
||||
SharedWithGroupID: uuid.MustParse("590f1006-15e6-4b21-a6e1-92e33af8a5c3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupInOrg",
|
||||
Query: "shared_with_group:wibble/wobble",
|
||||
Setup: func(t *testing.T, db database.Store) {
|
||||
org := dbgen.Organization(t, db, database.Organization{
|
||||
ID: uuid.MustParse("dbeb1bd5-dce6-459c-ab7b-b7f8b9b10467"),
|
||||
Name: "wibble",
|
||||
})
|
||||
dbgen.Group(t, db, database.Group{
|
||||
ID: uuid.MustParse("3c831688-0a5a-45a2-a796-f7648874df34"),
|
||||
Name: "wobble",
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
},
|
||||
Expected: database.GetWorkspacesParams{
|
||||
SharedWithGroupID: uuid.MustParse("3c831688-0a5a-45a2-a796-f7648874df34"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupID",
|
||||
Query: "shared_with_group:a7d1ba00-53c7-4aa6-92ea-83157dd57480",
|
||||
Setup: func(t *testing.T, db database.Store) {
|
||||
org := dbgen.Organization(t, db, database.Organization{
|
||||
ID: uuid.MustParse("8606620f-fee4-4c4e-83ba-f42db804139a"),
|
||||
})
|
||||
dbgen.Group(t, db, database.Group{
|
||||
ID: uuid.MustParse("a7d1ba00-53c7-4aa6-92ea-83157dd57480"),
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
},
|
||||
Expected: database.GetWorkspacesParams{
|
||||
SharedWithGroupID: uuid.MustParse("a7d1ba00-53c7-4aa6-92ea-83157dd57480"),
|
||||
},
|
||||
},
|
||||
|
||||
// Failures
|
||||
{
|
||||
@@ -354,6 +432,21 @@ func TestSearchWorkspace(t *testing.T) {
|
||||
Query: "param:foo:value",
|
||||
ExpectedErrorContains: "can only contain 1 ':'",
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupTooManySegments",
|
||||
Query: `shared_with_group:acme/devs/extra`,
|
||||
ExpectedErrorContains: "the filter must be in the pattern of <organization name>/<group name>",
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupEmptyOrg",
|
||||
Query: `shared_with_group:/devs`,
|
||||
ExpectedErrorContains: "invalid organization name",
|
||||
},
|
||||
{
|
||||
Name: "SharedWithGroupEmptyGroup",
|
||||
Query: `shared_with_group:acme/`,
|
||||
ExpectedErrorContains: "organization \"acme\" either does not exist",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
|
||||
Reference in New Issue
Block a user