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

Commit38ca987

Browse files
authored
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 /workspacesendpoint.- `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](coder/internal#1004)
1 parent40ffb79 commit38ca987

File tree

8 files changed

+407
-9
lines changed

8 files changed

+407
-9
lines changed

‎coderd/database/modelqueries.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
276276
arg.HasAITask,
277277
arg.HasExternalAgent,
278278
arg.Shared,
279+
arg.SharedWithUserID,
280+
arg.SharedWithGroupID,
279281
arg.RequesterID,
280282
arg.Offset,
281283
arg.Limit,

‎coderd/database/queries.sql.go‎

Lines changed: 21 additions & 6 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/database/queries/workspaces.sql‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,18 @@ WHERE
384384
(workspaces.user_acl!='{}'::jsonbORworkspaces.group_acl!='{}'::jsonb)=sqlc.narg('shared') ::boolean
385385
ELSE true
386386
END
387-
387+
-- Filter by shared_with_user_id
388+
AND CASE
389+
WHEN @shared_with_user_id :: uuid!='00000000-0000-0000-0000-000000000000'::uuid THEN
390+
workspaces.user_acl ? (@shared_with_user_id :: uuid) ::text
391+
ELSE true
392+
END
393+
-- Filter by shared_with_group_id
394+
AND CASE
395+
WHEN @shared_with_group_id :: uuid!='00000000-0000-0000-0000-000000000000'::uuid THEN
396+
workspaces.group_acl ? (@shared_with_group_id :: uuid) ::text
397+
ELSE true
398+
END
388399
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaces
389400
-- @authorize_filter
390401
), filtered_workspaces_orderAS (

‎coderd/searchquery/search.go‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ func Workspaces(ctx context.Context, db database.Store, query string, page coder
226226
filter.HasExternalAgent=parser.NullableBoolean(values, sql.NullBool{},"has_external_agent")
227227
filter.OrganizationID=parseOrganization(ctx,db,parser,values,"organization")
228228
filter.Shared=parser.NullableBoolean(values, sql.NullBool{},"shared")
229+
filter.SharedWithUserID=parseUser(ctx,db,parser,values,"shared_with_user")
230+
filter.SharedWithGroupID=parseGroup(ctx,db,parser,values,"shared_with_group")
229231

230232
typeparamMatchstruct {
231233
namestring
@@ -363,6 +365,85 @@ func parseOrganization(ctx context.Context, db database.Store, parser *httpapi.Q
363365
})
364366
}
365367

368+
funcparseUser(ctx context.Context,db database.Store,parser*httpapi.QueryParamParser,vals url.Values,queryParamstring) uuid.UUID {
369+
returnhttpapi.ParseCustom(parser,vals,uuid.Nil,queryParam,func(vstring) (uuid.UUID,error) {
370+
ifv=="" {
371+
returnuuid.Nil,nil
372+
}
373+
userID,err:=uuid.Parse(v)
374+
iferr==nil {
375+
returnuserID,nil
376+
}
377+
user,err:=db.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{
378+
Username:v,
379+
})
380+
iferr!=nil {
381+
returnuuid.Nil,xerrors.Errorf("user %q either does not exist, or you are unauthorized to view them",v)
382+
}
383+
returnuser.ID,nil
384+
})
385+
}
386+
387+
// Parse a group filter value into a group UUID.
388+
// Supported formats:
389+
// - <group-uuid>
390+
// - <organization-name>/<group-name>
391+
// - <group-name> (resolved in the default organization)
392+
funcparseGroup(ctx context.Context,db database.Store,parser*httpapi.QueryParamParser,vals url.Values,queryParamstring) uuid.UUID {
393+
returnhttpapi.ParseCustom(parser,vals,uuid.Nil,queryParam,func(vstring) (uuid.UUID,error) {
394+
ifv=="" {
395+
returnuuid.Nil,nil
396+
}
397+
groupID,err:=uuid.Parse(v)
398+
iferr==nil {
399+
returngroupID,nil
400+
}
401+
402+
vargroupNamestring
403+
varorg database.Organization
404+
parts:=strings.Split(v,"/")
405+
switchlen(parts) {
406+
case1:
407+
dbOrg,err:=db.GetDefaultOrganization(ctx)
408+
iferr!=nil {
409+
returnuuid.Nil,xerrors.New("fetching default organization")
410+
}
411+
org=dbOrg
412+
groupName=parts[0]
413+
case2:
414+
orgName:=parts[0]
415+
iferr:=codersdk.NameValid(orgName);err!=nil {
416+
returnuuid.Nil,xerrors.Errorf("invalid organization name %w",err)
417+
}
418+
dbOrg,err:=db.GetOrganizationByName(ctx, database.GetOrganizationByNameParams{
419+
Name:orgName,
420+
})
421+
iferr!=nil {
422+
returnuuid.Nil,xerrors.Errorf("organization %q either does not exist, or you are unauthorized to view it",orgName)
423+
}
424+
org=dbOrg
425+
426+
groupName=parts[1]
427+
428+
default:
429+
returnuuid.Nil,xerrors.New("invalid organization or group name, the filter must be in the pattern of <organization name>/<group name>")
430+
}
431+
432+
iferr:=codersdk.GroupNameValid(groupName);err!=nil {
433+
returnuuid.Nil,xerrors.Errorf("invalid group name %w",err)
434+
}
435+
436+
group,err:=db.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{
437+
OrganizationID:org.ID,
438+
Name:groupName,
439+
})
440+
iferr!=nil {
441+
returnuuid.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)
442+
}
443+
returngroup.ID,nil
444+
})
445+
}
446+
366447
// splitQueryParameterByDelimiter takes a query string and splits it into the individual elements
367448
// of the query. Each element is separated by a delimiter. All quoted strings are
368449
// kept as a single element.

‎coderd/searchquery/search_test.go‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,84 @@ func TestSearchWorkspace(t *testing.T) {
312312
},
313313
},
314314
},
315+
{
316+
Name:"SharedWithUser",
317+
Query:`shared_with_user:3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf`,
318+
Setup:func(t*testing.T,db database.Store) {
319+
dbgen.User(t,db, database.User{
320+
ID:uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
321+
})
322+
},
323+
Expected: database.GetWorkspacesParams{
324+
SharedWithUserID:uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
325+
},
326+
},
327+
{
328+
Name:"SharedWithUserByName",
329+
Query:`shared_with_user:wibble`,
330+
Setup:func(t*testing.T,db database.Store) {
331+
dbgen.User(t,db, database.User{
332+
ID:uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
333+
Username:"wibble",
334+
})
335+
},
336+
Expected: database.GetWorkspacesParams{
337+
SharedWithUserID:uuid.MustParse("3dd8b1b8-dff5-4b22-8ae9-c243ca136ecf"),
338+
},
339+
},
340+
{
341+
Name:"SharedWithGroupDefaultOrg",
342+
Query:"shared_with_group:wibble",
343+
Setup:func(t*testing.T,db database.Store) {
344+
org,err:=db.GetOrganizationByName(t.Context(), database.GetOrganizationByNameParams{
345+
Name:"coder",
346+
})
347+
require.NoError(t,err)
348+
349+
dbgen.Group(t,db, database.Group{
350+
ID:uuid.MustParse("590f1006-15e6-4b21-a6e1-92e33af8a5c3"),
351+
Name:"wibble",
352+
OrganizationID:org.ID,
353+
})
354+
},
355+
Expected: database.GetWorkspacesParams{
356+
SharedWithGroupID:uuid.MustParse("590f1006-15e6-4b21-a6e1-92e33af8a5c3"),
357+
},
358+
},
359+
{
360+
Name:"SharedWithGroupInOrg",
361+
Query:"shared_with_group:wibble/wobble",
362+
Setup:func(t*testing.T,db database.Store) {
363+
org:=dbgen.Organization(t,db, database.Organization{
364+
ID:uuid.MustParse("dbeb1bd5-dce6-459c-ab7b-b7f8b9b10467"),
365+
Name:"wibble",
366+
})
367+
dbgen.Group(t,db, database.Group{
368+
ID:uuid.MustParse("3c831688-0a5a-45a2-a796-f7648874df34"),
369+
Name:"wobble",
370+
OrganizationID:org.ID,
371+
})
372+
},
373+
Expected: database.GetWorkspacesParams{
374+
SharedWithGroupID:uuid.MustParse("3c831688-0a5a-45a2-a796-f7648874df34"),
375+
},
376+
},
377+
{
378+
Name:"SharedWithGroupID",
379+
Query:"shared_with_group:a7d1ba00-53c7-4aa6-92ea-83157dd57480",
380+
Setup:func(t*testing.T,db database.Store) {
381+
org:=dbgen.Organization(t,db, database.Organization{
382+
ID:uuid.MustParse("8606620f-fee4-4c4e-83ba-f42db804139a"),
383+
})
384+
dbgen.Group(t,db, database.Group{
385+
ID:uuid.MustParse("a7d1ba00-53c7-4aa6-92ea-83157dd57480"),
386+
OrganizationID:org.ID,
387+
})
388+
},
389+
Expected: database.GetWorkspacesParams{
390+
SharedWithGroupID:uuid.MustParse("a7d1ba00-53c7-4aa6-92ea-83157dd57480"),
391+
},
392+
},
315393

316394
// Failures
317395
{
@@ -354,6 +432,21 @@ func TestSearchWorkspace(t *testing.T) {
354432
Query:"param:foo:value",
355433
ExpectedErrorContains:"can only contain 1 ':'",
356434
},
435+
{
436+
Name:"SharedWithGroupTooManySegments",
437+
Query:`shared_with_group:acme/devs/extra`,
438+
ExpectedErrorContains:"the filter must be in the pattern of <organization name>/<group name>",
439+
},
440+
{
441+
Name:"SharedWithGroupEmptyOrg",
442+
Query:`shared_with_group:/devs`,
443+
ExpectedErrorContains:"invalid organization name",
444+
},
445+
{
446+
Name:"SharedWithGroupEmptyGroup",
447+
Query:`shared_with_group:acme/`,
448+
ExpectedErrorContains:"organization\"acme\" either does not exist",
449+
},
357450
}
358451

359452
for_,c:=rangetestCases {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp