diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2d18a69576..083a5b90c0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17426,6 +17426,9 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.SlimRole" } }, + "is_service_account": { + "type": "boolean" + }, "last_seen_at": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 075536ae96..067eb22186 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15851,6 +15851,9 @@ "$ref": "#/definitions/codersdk.SlimRole" } }, + "is_service_account": { + "type": "boolean" + }, "last_seen_at": { "type": "string", "format": "date-time" diff --git a/coderd/coderdtest/users.go b/coderd/coderdtest/users.go index 2732c49673..f5a798b06e 100644 --- a/coderd/coderdtest/users.go +++ b/coderd/coderdtest/users.go @@ -210,6 +210,14 @@ func UsersFilter( users = append(users, user) } + // Add some service accounts. + for range 3 { + _, user := CreateAnotherUserMutators(t, client, orgID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { + r.ServiceAccount = true + }) + users = append(users, user) + } + hashedPassword, err := userpassword.Hash("SomeStrongPassword!") require.NoError(t, err) @@ -560,6 +568,24 @@ func UsersFilter( return u.Status == codersdk.UserStatusSuspended && u.LoginType == codersdk.LoginTypeNone }, }, + { + Name: "IsServiceAccount", + Filter: codersdk.UsersRequest{ + Search: "service_account:true", + }, + FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool { + return u.IsServiceAccount + }, + }, + { + Name: "IsNotServiceAccount", + Filter: codersdk.UsersRequest{ + Search: "service_account:false", + }, + FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool { + return !u.IsServiceAccount + }, + }, } for _, c := range testCases { diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 6a8aed8108..beb7a3de44 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -422,6 +422,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, arg.IncludeSystem, arg.GithubComUserID, pq.Array(arg.LoginType), + arg.IsServiceAccount, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3543d1f8e7..6ea25cf4ad 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7797,11 +7797,12 @@ WHERE user_created_at >= $10 ELSE true END - -- Filter by system type + -- Filter by system type AND CASE WHEN $11::bool THEN TRUE ELSE user_is_system = false END + -- Filter by github.com user ID AND CASE WHEN $12 :: bigint != 0 THEN user_github_com_user_id = $12 @@ -7813,31 +7814,38 @@ WHERE user_login_type = ANY($13 :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN $14 :: boolean IS NOT NULL THEN + user_is_service_account = $14 :: boolean + ELSE true + END -- End of filters ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(user_username) ASC OFFSET $14 + LOWER(user_username) ASC OFFSET $15 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($15 :: int, 0) + NULLIF($16 :: int, 0) ` type GetGroupMembersByGroupIDPaginatedParams struct { - GroupID uuid.UUID `db:"group_id" json:"group_id"` - AfterID uuid.UUID `db:"after_id" json:"after_id"` - Search string `db:"search" json:"search"` - Name string `db:"name" json:"name"` - Status []UserStatus `db:"status" json:"status"` - RbacRole []string `db:"rbac_role" json:"rbac_role"` - LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` - LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` - CreatedBefore time.Time `db:"created_before" json:"created_before"` - CreatedAfter time.Time `db:"created_after" json:"created_after"` - IncludeSystem bool `db:"include_system" json:"include_system"` - GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` - LoginType []LoginType `db:"login_type" json:"login_type"` - OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` - LimitOpt int32 `db:"limit_opt" json:"limit_opt"` + GroupID uuid.UUID `db:"group_id" json:"group_id"` + AfterID uuid.UUID `db:"after_id" json:"after_id"` + Search string `db:"search" json:"search"` + Name string `db:"name" json:"name"` + Status []UserStatus `db:"status" json:"status"` + RbacRole []string `db:"rbac_role" json:"rbac_role"` + LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` + LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` + CreatedBefore time.Time `db:"created_before" json:"created_before"` + CreatedAfter time.Time `db:"created_after" json:"created_after"` + IncludeSystem bool `db:"include_system" json:"include_system"` + GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` + LoginType []LoginType `db:"login_type" json:"login_type"` + IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"` + OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` + LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } type GetGroupMembersByGroupIDPaginatedRow struct { @@ -7879,6 +7887,7 @@ func (q *sqlQuerier) GetGroupMembersByGroupIDPaginated(ctx context.Context, arg arg.IncludeSystem, arg.GithubComUserID, pq.Array(arg.LoginType), + arg.IsServiceAccount, arg.OffsetOpt, arg.LimitOpt, ) @@ -12745,7 +12754,7 @@ const organizationMembers = `-- name: OrganizationMembers :many SELECT organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles, users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", - users.last_seen_at, users.status, users.login_type, + users.last_seen_at, users.status, users.login_type, users.is_service_account, users.created_at as user_created_at, users.updated_at as user_updated_at FROM organization_members @@ -12795,6 +12804,7 @@ type OrganizationMembersRow struct { LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` Status UserStatus `db:"status" json:"status"` LoginType LoginType `db:"login_type" json:"login_type"` + IsServiceAccount bool `db:"is_service_account" json:"is_service_account"` UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"` UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"` } @@ -12831,6 +12841,7 @@ func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMe &i.LastSeenAt, &i.Status, &i.LoginType, + &i.IsServiceAccount, &i.UserCreatedAt, &i.UserUpdatedAt, ); err != nil { @@ -12851,7 +12862,7 @@ const paginatedOrganizationMembers = `-- name: PaginatedOrganizationMembers :man SELECT organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles, users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", - users.last_seen_at, users.status, users.login_type, + users.last_seen_at, users.status, users.login_type, users.is_service_account, users.created_at as user_created_at, users.updated_at as user_updated_at, COUNT(*) OVER() AS count FROM @@ -12956,31 +12967,38 @@ WHERE users.login_type = ANY($13 :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN $14 :: boolean IS NOT NULL THEN + users.is_service_account = $14 :: boolean + ELSE true + END -- End of filters ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(users.username) ASC OFFSET $14 + LOWER(users.username) ASC OFFSET $15 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($15 :: int, 0) + NULLIF($16 :: int, 0) ` type PaginatedOrganizationMembersParams struct { - AfterID uuid.UUID `db:"after_id" json:"after_id"` - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - Search string `db:"search" json:"search"` - Name string `db:"name" json:"name"` - Status []UserStatus `db:"status" json:"status"` - RbacRole []string `db:"rbac_role" json:"rbac_role"` - LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` - LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` - CreatedBefore time.Time `db:"created_before" json:"created_before"` - CreatedAfter time.Time `db:"created_after" json:"created_after"` - IncludeSystem bool `db:"include_system" json:"include_system"` - GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` - LoginType []LoginType `db:"login_type" json:"login_type"` - OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` - LimitOpt int32 `db:"limit_opt" json:"limit_opt"` + AfterID uuid.UUID `db:"after_id" json:"after_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + Search string `db:"search" json:"search"` + Name string `db:"name" json:"name"` + Status []UserStatus `db:"status" json:"status"` + RbacRole []string `db:"rbac_role" json:"rbac_role"` + LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` + LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` + CreatedBefore time.Time `db:"created_before" json:"created_before"` + CreatedAfter time.Time `db:"created_after" json:"created_after"` + IncludeSystem bool `db:"include_system" json:"include_system"` + GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` + LoginType []LoginType `db:"login_type" json:"login_type"` + IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"` + OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` + LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } type PaginatedOrganizationMembersRow struct { @@ -12993,6 +13011,7 @@ type PaginatedOrganizationMembersRow struct { LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` Status UserStatus `db:"status" json:"status"` LoginType LoginType `db:"login_type" json:"login_type"` + IsServiceAccount bool `db:"is_service_account" json:"is_service_account"` UserCreatedAt time.Time `db:"user_created_at" json:"user_created_at"` UserUpdatedAt time.Time `db:"user_updated_at" json:"user_updated_at"` Count int64 `db:"count" json:"count"` @@ -13013,6 +13032,7 @@ func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg Pagin arg.IncludeSystem, arg.GithubComUserID, pq.Array(arg.LoginType), + arg.IsServiceAccount, arg.OffsetOpt, arg.LimitOpt, ) @@ -13037,6 +13057,7 @@ func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg Pagin &i.LastSeenAt, &i.Status, &i.LoginType, + &i.IsServiceAccount, &i.UserCreatedAt, &i.UserUpdatedAt, &i.Count, @@ -21843,11 +21864,12 @@ WHERE created_at >= $9 ELSE true END - AND CASE - WHEN $10::bool THEN TRUE - ELSE - is_system = false + -- Filter by system type + AND CASE + WHEN $10::bool THEN TRUE + ELSE is_system = false END + -- Filter by github.com user ID AND CASE WHEN $11 :: bigint != 0 THEN github_com_user_id = $11 @@ -21859,33 +21881,40 @@ WHERE login_type = ANY($12 :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN $13 :: boolean IS NOT NULL THEN + is_service_account = $13 :: boolean + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers -- @authorize_filter ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(username) ASC OFFSET $13 + LOWER(username) ASC OFFSET $14 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($14 :: int, 0) + NULLIF($15 :: int, 0) ` type GetUsersParams struct { - AfterID uuid.UUID `db:"after_id" json:"after_id"` - Search string `db:"search" json:"search"` - Name string `db:"name" json:"name"` - Status []UserStatus `db:"status" json:"status"` - RbacRole []string `db:"rbac_role" json:"rbac_role"` - LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` - LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` - CreatedBefore time.Time `db:"created_before" json:"created_before"` - CreatedAfter time.Time `db:"created_after" json:"created_after"` - IncludeSystem bool `db:"include_system" json:"include_system"` - GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` - LoginType []LoginType `db:"login_type" json:"login_type"` - OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` - LimitOpt int32 `db:"limit_opt" json:"limit_opt"` + AfterID uuid.UUID `db:"after_id" json:"after_id"` + Search string `db:"search" json:"search"` + Name string `db:"name" json:"name"` + Status []UserStatus `db:"status" json:"status"` + RbacRole []string `db:"rbac_role" json:"rbac_role"` + LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` + LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` + CreatedBefore time.Time `db:"created_before" json:"created_before"` + CreatedAfter time.Time `db:"created_after" json:"created_after"` + IncludeSystem bool `db:"include_system" json:"include_system"` + GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` + LoginType []LoginType `db:"login_type" json:"login_type"` + IsServiceAccount sql.NullBool `db:"is_service_account" json:"is_service_account"` + OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` + LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } type GetUsersRow struct { @@ -21927,6 +21956,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse arg.IncludeSystem, arg.GithubComUserID, pq.Array(arg.LoginType), + arg.IsServiceAccount, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries/groupmembers.sql b/coderd/database/queries/groupmembers.sql index b2f9d5fee8..4e5469317a 100644 --- a/coderd/database/queries/groupmembers.sql +++ b/coderd/database/queries/groupmembers.sql @@ -97,11 +97,12 @@ WHERE user_created_at >= @created_after ELSE true END - -- Filter by system type + -- Filter by system type AND CASE WHEN @include_system::bool THEN TRUE ELSE user_is_system = false END + -- Filter by github.com user ID AND CASE WHEN @github_com_user_id :: bigint != 0 THEN user_github_com_user_id = @github_com_user_id @@ -113,6 +114,12 @@ WHERE user_login_type = ANY(@login_type :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN sqlc.narg('is_service_account') :: boolean IS NOT NULL THEN + user_is_service_account = sqlc.narg('is_service_account') :: boolean + ELSE true + END -- End of filters ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 7154a68d76..78e7e31163 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -6,7 +6,7 @@ SELECT sqlc.embed(organization_members), users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", - users.last_seen_at, users.status, users.login_type, + users.last_seen_at, users.status, users.login_type, users.is_service_account, users.created_at as user_created_at, users.updated_at as user_updated_at FROM organization_members @@ -85,7 +85,7 @@ RETURNING *; SELECT sqlc.embed(organization_members), users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", - users.last_seen_at, users.status, users.login_type, + users.last_seen_at, users.status, users.login_type, users.is_service_account, users.created_at as user_created_at, users.updated_at as user_updated_at, COUNT(*) OVER() AS count FROM @@ -190,6 +190,12 @@ WHERE users.login_type = ANY(@login_type :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN sqlc.narg('is_service_account') :: boolean IS NOT NULL THEN + users.is_service_account = sqlc.narg('is_service_account') :: boolean + ELSE true + END -- End of filters ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 8572a2ad8b..85ff08b9c6 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -344,11 +344,12 @@ WHERE created_at >= @created_after ELSE true END - AND CASE - WHEN @include_system::bool THEN TRUE - ELSE - is_system = false + -- Filter by system type + AND CASE + WHEN @include_system::bool THEN TRUE + ELSE is_system = false END + -- Filter by github.com user ID AND CASE WHEN @github_com_user_id :: bigint != 0 THEN github_com_user_id = @github_com_user_id @@ -360,6 +361,12 @@ WHERE login_type = ANY(@login_type :: login_type[]) ELSE true END + -- Filter by service account. + AND CASE + WHEN sqlc.narg('is_service_account') :: boolean IS NOT NULL THEN + is_service_account = sqlc.narg('is_service_account') :: boolean + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers diff --git a/coderd/members.go b/coderd/members.go index 6de4cde143..8044395db8 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -270,19 +270,20 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { } paginatedMemberRows, err := api.Database.PaginatedOrganizationMembers(ctx, database.PaginatedOrganizationMembersParams{ - AfterID: paginationParams.AfterID, - OrganizationID: organization.ID, - IncludeSystem: false, - Search: userFilterParams.Search, - Name: userFilterParams.Name, - Status: userFilterParams.Status, - RbacRole: userFilterParams.RbacRole, - LastSeenBefore: userFilterParams.LastSeenBefore, - LastSeenAfter: userFilterParams.LastSeenAfter, - CreatedAfter: userFilterParams.CreatedAfter, - CreatedBefore: userFilterParams.CreatedBefore, - GithubComUserID: userFilterParams.GithubComUserID, - LoginType: userFilterParams.LoginType, + AfterID: paginationParams.AfterID, + OrganizationID: organization.ID, + IncludeSystem: false, + Search: userFilterParams.Search, + Name: userFilterParams.Name, + Status: userFilterParams.Status, + IsServiceAccount: userFilterParams.IsServiceAccount, + RbacRole: userFilterParams.RbacRole, + LastSeenBefore: userFilterParams.LastSeenBefore, + LastSeenAfter: userFilterParams.LastSeenAfter, + CreatedAfter: userFilterParams.CreatedAfter, + CreatedBefore: userFilterParams.CreatedBefore, + GithubComUserID: userFilterParams.GithubComUserID, + LoginType: userFilterParams.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 @@ -308,6 +309,7 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { GlobalRoles: pRow.GlobalRoles, LastSeenAt: pRow.LastSeenAt, Status: pRow.Status, + IsServiceAccount: pRow.IsServiceAccount, LoginType: pRow.LoginType, UserCreatedAt: pRow.UserCreatedAt, UserUpdatedAt: pRow.UserUpdatedAt, @@ -530,6 +532,7 @@ func convertOrganizationMembersWithUserData(ctx context.Context, db database.Sto GlobalRoles: db2sdk.SlimRolesFromNames(rows[i].GlobalRoles), LastSeenAt: rows[i].LastSeenAt, Status: codersdk.UserStatus(rows[i].Status), + IsServiceAccount: rows[i].IsServiceAccount, LoginType: codersdk.LoginType(rows[i].LoginType), UserCreatedAt: rows[i].UserCreatedAt, UserUpdatedAt: rows[i].UserUpdatedAt, diff --git a/coderd/members_test.go b/coderd/members_test.go index 086d960580..0eee9d1db0 100644 --- a/coderd/members_test.go +++ b/coderd/members_test.go @@ -190,11 +190,12 @@ func orgMemberToReducedUser(user codersdk.OrganizationMemberWithUserData) coders Name: user.Name, AvatarURL: user.AvatarURL, }, - Email: user.Email, - CreatedAt: user.UserCreatedAt, - UpdatedAt: user.UserUpdatedAt, - LastSeenAt: user.LastSeenAt, - Status: user.Status, - LoginType: user.LoginType, + Email: user.Email, + CreatedAt: user.UserCreatedAt, + UpdatedAt: user.UserUpdatedAt, + LastSeenAt: user.LastSeenAt, + Status: user.Status, + IsServiceAccount: user.IsServiceAccount, + LoginType: user.LoginType, } } diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 462562ae28..814ba985ab 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -155,16 +155,17 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) { parser := httpapi.NewQueryParamParser() filter := database.GetUsersParams{ - Search: parser.String(values, "", "search"), - Name: parser.String(values, "", "name"), - Status: httpapi.ParseCustomList(parser, values, []database.UserStatus{}, "status", httpapi.ParseEnum[database.UserStatus]), - RbacRole: parser.Strings(values, []string{}, "role"), - LastSeenAfter: parser.Time3339Nano(values, time.Time{}, "last_seen_after"), - LastSeenBefore: parser.Time3339Nano(values, time.Time{}, "last_seen_before"), - CreatedAfter: parser.Time3339Nano(values, time.Time{}, "created_after"), - CreatedBefore: parser.Time3339Nano(values, time.Time{}, "created_before"), - GithubComUserID: parser.Int64(values, 0, "github_com_user_id"), - LoginType: httpapi.ParseCustomList(parser, values, []database.LoginType{}, "login_type", httpapi.ParseEnum[database.LoginType]), + Search: parser.String(values, "", "search"), + Name: parser.String(values, "", "name"), + Status: httpapi.ParseCustomList(parser, values, []database.UserStatus{}, "status", httpapi.ParseEnum[database.UserStatus]), + IsServiceAccount: parser.NullableBoolean(values, sql.NullBool{}, "service_account"), + RbacRole: parser.Strings(values, []string{}, "role"), + LastSeenAfter: parser.Time3339Nano(values, time.Time{}, "last_seen_after"), + LastSeenBefore: parser.Time3339Nano(values, time.Time{}, "last_seen_before"), + CreatedAfter: parser.Time3339Nano(values, time.Time{}, "created_after"), + CreatedBefore: parser.Time3339Nano(values, time.Time{}, "created_before"), + GithubComUserID: parser.Int64(values, 0, "github_com_user_id"), + LoginType: httpapi.ParseCustomList(parser, values, []database.LoginType{}, "login_type", httpapi.ParseEnum[database.LoginType]), } parser.ErrorExcessParams(values) return filter, parser.Errors diff --git a/coderd/users.go b/coderd/users.go index 64a1508675..d00ebe5163 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -353,17 +353,18 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us } userRows, err := api.Database.GetUsers(ctx, database.GetUsersParams{ - AfterID: paginationParams.AfterID, - Search: params.Search, - Name: params.Name, - Status: params.Status, - RbacRole: params.RbacRole, - LastSeenBefore: params.LastSeenBefore, - LastSeenAfter: params.LastSeenAfter, - CreatedAfter: params.CreatedAfter, - CreatedBefore: params.CreatedBefore, - GithubComUserID: params.GithubComUserID, - LoginType: params.LoginType, + AfterID: paginationParams.AfterID, + Search: params.Search, + Name: params.Name, + Status: params.Status, + IsServiceAccount: params.IsServiceAccount, + RbacRole: params.RbacRole, + LastSeenBefore: params.LastSeenBefore, + LastSeenAfter: params.LastSeenAfter, + CreatedAfter: params.CreatedAfter, + CreatedBefore: params.CreatedBefore, + GithubComUserID: params.GithubComUserID, + LoginType: params.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 555a9100c6..4f9638c705 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -82,6 +82,7 @@ type OrganizationMemberWithUserData struct { LastSeenAt time.Time `table:"last seen at" json:"last_seen_at,omitempty" format:"date-time"` UserCreatedAt time.Time `table:"user created at" json:"user_created_at" format:"date-time"` UserUpdatedAt time.Time `table:"user updated at" json:"user_updated_at" format:"date-time"` + IsServiceAccount bool `json:"is_service_account,omitempty"` GlobalRoles []SlimRole `json:"global_roles"` OrganizationMember `table:"m,recursive_inline"` } diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index c1c528542b..b49ac35905 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -192,6 +192,7 @@ to use the Coder's filter query: `created_before:"2023-01-18T00:00:00Z" created_after:"2023-01-01T23:59:59Z"` - To find users who login using Github: `login_type:github` +- To find service accounts: `service_account:true`. The following filters are supported: @@ -206,6 +207,9 @@ The following filters are supported: - `created_before` and `created_after` - The time a user was created. Uses the RFC3339Nano format. - `login_type` - Represents the login type of the user. Refer to the [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) for a list of supported values +- `service_account` - Can be either `true` to only include service accounts or + `false` to filter them out. If omitted, both service and regular accounts and + are returned. ## Edit a user's profile diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index ca97f41682..efcc8a312b 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -36,6 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members "organization_id": "string" } ], + "is_service_account": true, "last_seen_at": "2019-08-24T14:15:22Z", "login_type": "", "name": "string", @@ -67,27 +68,28 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members Status Code **200** -| Name | Type | Required | Restrictions | Description | -|----------------------|------------------------------------------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» avatar_url` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» email` | string | false | | | -| `» global_roles` | array | false | | | -| `»» display_name` | string | false | | | -| `»» name` | string | false | | | -| `»» organization_id` | string | false | | | -| `» last_seen_at` | string(date-time) | false | | | -| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» roles` | array | false | | | -| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» user_created_at` | string(date-time) | false | | | -| `» user_id` | string(uuid) | false | | | -| `» user_updated_at` | string(date-time) | false | | | -| `» username` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|------------------------------------------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» avatar_url` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» email` | string | false | | | +| `» global_roles` | array | false | | | +| `»» display_name` | string | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string | false | | | +| `» is_service_account` | boolean | false | | | +| `» last_seen_at` | string(date-time) | false | | | +| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» roles` | array | false | | | +| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» user_created_at` | string(date-time) | false | | | +| `» user_id` | string(uuid) | false | | | +| `» user_updated_at` | string(date-time) | false | | | +| `» username` | string | false | | | #### Enumerated Values @@ -593,6 +595,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members "organization_id": "string" } ], + "is_service_account": true, "last_seen_at": "2019-08-24T14:15:22Z", "login_type": "", "name": "string", @@ -799,6 +802,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat "organization_id": "string" } ], + "is_service_account": true, "last_seen_at": "2019-08-24T14:15:22Z", "login_type": "", "name": "string", @@ -832,29 +836,30 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat Status Code **200** -| Name | Type | Required | Restrictions | Description | -|-----------------------|------------------------------------------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» count` | integer | false | | | -| `» members` | array | false | | | -| `»» avatar_url` | string | false | | | -| `»» created_at` | string(date-time) | false | | | -| `»» email` | string | false | | | -| `»» global_roles` | array | false | | | -| `»»» display_name` | string | false | | | -| `»»» name` | string | false | | | -| `»»» organization_id` | string | false | | | -| `»» last_seen_at` | string(date-time) | false | | | -| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | -| `»» name` | string | false | | | -| `»» organization_id` | string(uuid) | false | | | -| `»» roles` | array | false | | | -| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | -| `»» updated_at` | string(date-time) | false | | | -| `»» user_created_at` | string(date-time) | false | | | -| `»» user_id` | string(uuid) | false | | | -| `»» user_updated_at` | string(date-time) | false | | | -| `»» username` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-------------------------|------------------------------------------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» count` | integer | false | | | +| `» members` | array | false | | | +| `»» avatar_url` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» email` | string | false | | | +| `»» global_roles` | array | false | | | +| `»»» display_name` | string | false | | | +| `»»» name` | string | false | | | +| `»»» organization_id` | string | false | | | +| `»» is_service_account` | boolean | false | | | +| `»» last_seen_at` | string(date-time) | false | | | +| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» roles` | array | false | | | +| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» user_created_at` | string(date-time) | false | | | +| `»» user_id` | string(uuid) | false | | | +| `»» user_updated_at` | string(date-time) | false | | | +| `»» username` | string | false | | | #### Enumerated Values diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 2bea8f2b23..6c328e8042 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6127,6 +6127,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| "organization_id": "string" } ], + "is_service_account": true, "last_seen_at": "2019-08-24T14:15:22Z", "login_type": "", "name": "string", @@ -6149,23 +6150,24 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------|-------------------------------------------------|----------|--------------|-------------| -| `avatar_url` | string | false | | | -| `created_at` | string | false | | | -| `email` | string | false | | | -| `global_roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | | -| `last_seen_at` | string | false | | | -| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | | -| `name` | string | false | | | -| `organization_id` | string | false | | | -| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | | -| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | | -| `updated_at` | string | false | | | -| `user_created_at` | string | false | | | -| `user_id` | string | false | | | -| `user_updated_at` | string | false | | | -| `username` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|----------------------|-------------------------------------------------|----------|--------------|-------------| +| `avatar_url` | string | false | | | +| `created_at` | string | false | | | +| `email` | string | false | | | +| `global_roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | | +| `is_service_account` | boolean | false | | | +| `last_seen_at` | string | false | | | +| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | | +| `name` | string | false | | | +| `organization_id` | string | false | | | +| `roles` | array of [codersdk.SlimRole](#codersdkslimrole) | false | | | +| `status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | | +| `updated_at` | string | false | | | +| `user_created_at` | string | false | | | +| `user_id` | string | false | | | +| `user_updated_at` | string | false | | | +| `username` | string | false | | | #### Enumerated Values @@ -6429,6 +6431,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| "organization_id": "string" } ], + "is_service_account": true, "last_seen_at": "2019-08-24T14:15:22Z", "login_type": "", "name": "string", diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index c14cf821b2..238bc98f3c 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -491,19 +491,20 @@ func (api *API) groupMembers(rw http.ResponseWriter, r *http.Request) { } members, err := api.Database.GetGroupMembersByGroupIDPaginated(ctx, database.GetGroupMembersByGroupIDPaginatedParams{ - AfterID: paginationParams.AfterID, - GroupID: group.ID, - IncludeSystem: false, - Search: userFilterParams.Search, - Name: userFilterParams.Name, - Status: userFilterParams.Status, - RbacRole: userFilterParams.RbacRole, - LastSeenBefore: userFilterParams.LastSeenBefore, - LastSeenAfter: userFilterParams.LastSeenAfter, - CreatedAfter: userFilterParams.CreatedAfter, - CreatedBefore: userFilterParams.CreatedBefore, - GithubComUserID: userFilterParams.GithubComUserID, - LoginType: userFilterParams.LoginType, + AfterID: paginationParams.AfterID, + GroupID: group.ID, + IncludeSystem: false, + Search: userFilterParams.Search, + Name: userFilterParams.Name, + Status: userFilterParams.Status, + IsServiceAccount: userFilterParams.IsServiceAccount, + RbacRole: userFilterParams.RbacRole, + LastSeenBefore: userFilterParams.LastSeenBefore, + LastSeenAfter: userFilterParams.LastSeenAfter, + CreatedAfter: userFilterParams.CreatedAfter, + CreatedBefore: userFilterParams.CreatedBefore, + GithubComUserID: userFilterParams.GithubComUserID, + LoginType: userFilterParams.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4cb3cf5087..165f91b146 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4592,6 +4592,7 @@ export interface OrganizationMemberWithUserData extends OrganizationMember { readonly last_seen_at?: string; readonly user_created_at: string; readonly user_updated_at: string; + readonly is_service_account?: boolean; readonly global_roles: readonly SlimRole[]; } diff --git a/site/src/components/Filter/UsersFilter.tsx b/site/src/components/Filter/UsersFilter.tsx index abbc1757bf..86e928af19 100644 --- a/site/src/components/Filter/UsersFilter.tsx +++ b/site/src/components/Filter/UsersFilter.tsx @@ -13,6 +13,7 @@ import { docs } from "utils/docs"; const userFilterQuery = { active: "status:active", + serviceAccount: "service_account:true", all: "", }; @@ -51,6 +52,7 @@ type StatusFilterMenu = ReturnType; const PRESET_FILTERS = [ { query: userFilterQuery.active, name: "Active users" }, + { query: userFilterQuery.serviceAccount, name: "Service accounts" }, { query: userFilterQuery.all, name: "All users" }, ];