mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: resolve organization member visibility issue during owned work sharing (#21657)
The workspace sharing autocomplete was using the site-wide /api/v2/users
endpoint which requires user:read permission. Regular org members don't
have this permission, so they couldn't see other members to share with.
## Sharing Scope
* This iteration of shared workspaces is slated for beta, and the
currently understood use case does not include cross-org workspace
sharing. This can be addressed later if necessary.
## What's Changed
* Changed to use /api/v2/organizations/{org}/members instead, which only
requires organization_member:read permission (already granted to org
members when workspace sharing is enabled).
* Added `OrganizationMemberWithUserData` to the `UserLike` union to
allow for more flexibility in differentiating groups from users
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
Group,
|
||||
OrganizationMemberWithUserData,
|
||||
ReducedUser,
|
||||
User,
|
||||
WorkspaceUser,
|
||||
@@ -8,7 +9,11 @@ import type {
|
||||
/**
|
||||
* Union of all user-like types that can be distinguished from Group.
|
||||
*/
|
||||
type UserLike = User | ReducedUser | WorkspaceUser;
|
||||
type UserLike =
|
||||
| User
|
||||
| ReducedUser
|
||||
| WorkspaceUser
|
||||
| OrganizationMemberWithUserData;
|
||||
|
||||
/**
|
||||
* Type guard to check if the value is a Group.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { groupsByOrganization } from "api/queries/groups";
|
||||
import { users } from "api/queries/users";
|
||||
import type { Group, User } from "api/typesGenerated";
|
||||
import { organizationMembers } from "api/queries/organizations";
|
||||
import type { Group, OrganizationMemberWithUserData } from "api/typesGenerated";
|
||||
import { Autocomplete } from "components/Autocomplete/Autocomplete";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import { getGroupSubtitle, isGroup } from "modules/groups";
|
||||
import { type FC, useState } from "react";
|
||||
import { keepPreviousData, useQuery } from "react-query";
|
||||
import { prepareQuery } from "utils/filters";
|
||||
|
||||
type AutocompleteOption = User | Group;
|
||||
type OrganizationMember = OrganizationMemberWithUserData & { id: string };
|
||||
type AutocompleteOption = OrganizationMember | Group;
|
||||
export type UserOrGroupAutocompleteValue = AutocompleteOption | null;
|
||||
|
||||
type ExcludableOption = { id?: string | null } | null;
|
||||
@@ -21,6 +22,13 @@ type UserOrGroupAutocompleteProps = {
|
||||
exclude: ExcludableOption[];
|
||||
};
|
||||
|
||||
const normalizeMember = (
|
||||
member: OrganizationMemberWithUserData,
|
||||
): OrganizationMember => ({
|
||||
...member,
|
||||
id: member.user_id,
|
||||
});
|
||||
|
||||
export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
@@ -37,11 +45,11 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const usersQuery = useQuery({
|
||||
...users({
|
||||
q: prepareQuery(encodeURI(inputValue)),
|
||||
limit: 25,
|
||||
}),
|
||||
// Use org members endpoint instead of site-wide /users endpoint.
|
||||
// This allows regular org members to see other members in their org
|
||||
// for workspace sharing, without needing site-wide user:read permission.
|
||||
const membersQuery = useQuery({
|
||||
...organizationMembers(organizationId),
|
||||
enabled: open,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
@@ -53,6 +61,8 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
|
||||
});
|
||||
|
||||
const filterValue = inputValue.trim().toLowerCase();
|
||||
|
||||
// Filter groups by search input (client-side filtering).
|
||||
const groupOptions = groupsQuery.data
|
||||
? groupsQuery.data.filter((group) => {
|
||||
if (!filterValue) {
|
||||
@@ -63,13 +73,28 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
|
||||
})
|
||||
: [];
|
||||
|
||||
// Filter members by search input (client-side filtering since org members
|
||||
// endpoint doesn't support search params).
|
||||
const userOptions = membersQuery.data?.members
|
||||
? membersQuery.data.members
|
||||
.filter((member) => {
|
||||
if (!filterValue) {
|
||||
return true;
|
||||
}
|
||||
const haystack =
|
||||
`${member.name ?? ""} ${member.username} ${member.email}`.toLowerCase();
|
||||
return haystack.includes(filterValue);
|
||||
})
|
||||
.map(normalizeMember)
|
||||
: [];
|
||||
|
||||
const excludeIds = exclude
|
||||
.map((optionToExclude) => optionToExclude?.id)
|
||||
.filter((id): id is string => Boolean(id));
|
||||
|
||||
const options: AutocompleteOption[] = [
|
||||
...groupOptions,
|
||||
...(usersQuery.data?.users ?? []),
|
||||
...userOptions,
|
||||
].filter((result) => !excludeIds.includes(result.id));
|
||||
|
||||
return (
|
||||
@@ -102,7 +127,7 @@ export const UserOrGroupAutocomplete: FC<UserOrGroupAutocompleteProps> = ({
|
||||
onOpenChange={handleOpenChange}
|
||||
inputValue={inputValue}
|
||||
onInputChange={setInputValue}
|
||||
loading={usersQuery.isFetching || groupsQuery.isFetching}
|
||||
loading={membersQuery.isFetching || groupsQuery.isFetching}
|
||||
placeholder="Search for user or group"
|
||||
noOptionsText="No users or groups found"
|
||||
className="w-80"
|
||||
|
||||
Reference in New Issue
Block a user