Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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
8 changes: 7 additions & 1 deletion enterprise/coderd/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,15 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) {
})
return
}
// Unauthorized errors return a 404 to not leak information about the
// existence of resources.
if httpapi.IsUnauthorizedError(err) {
httpapi.ResourceNotFound(rw)
return
}
if httpapi.Is404Error(err) {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Failed to add or remove non-existent group member",
Message: "Failed to update group.",
Detail: err.Error(),
})
return
Expand Down
46 changes: 46 additions & 0 deletions enterprise/coderd/groups_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,52 @@ func TestPatchGroup(t *testing.T) {
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
})

// A group member can read the group but must not be able to update it.
// The update must be rejected as an authorization failure that returns a
// 404 to not leak resource existence.
t.Run("MemberWithoutUpdatePermission", func(t *testing.T) {
t.Parallel()

client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
}})
userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleUserAdmin())
memberClient, member := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)

group, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
Name: "hi",
})
require.NoError(t, err)

// Make the actor a member of the group so it can read the group but
// still lacks group:update.
_, err = userAdminClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{member.ID.String()},
})
require.NoError(t, err)

// Updating the group budget must fail as unauthorized (404).
_, err = memberClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
QuotaAllowance: ptr.Ref(20),
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())

// Adding a member must also fail as unauthorized (404).
_, err = memberClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
AddUsers: []string{user.UserID.String()},
})
require.Error(t, err)
cerr, ok = codersdk.AsError(err)
require.True(t, ok)
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})

t.Run("MalformedUUID", func(t *testing.T) {
t.Parallel()

Expand Down
42 changes: 42 additions & 0 deletions site/src/pages/GroupsPage/GroupPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
} from "#/testHelpers/storybook";
import GroupMembersPage from "./GroupMembersPage";
import GroupPage from "./GroupPage";
import GroupSettingsPage from "./GroupSettingsPage";

const meta: Meta<typeof GroupPage> = {
title: "pages/OrganizationGroupsPage/GroupPage",
Expand Down Expand Up @@ -208,6 +209,47 @@ export const NoUpdatePermission: Story = {
},
};

/**
* The settings page is reachable by direct URL even when the settings tab is
* hidden. Without group:update, it must render the permission dialog instead
* of the editable form.
*/
export const SettingsWithoutUpdatePermission: Story = {
parameters: {
reactRouter: reactRouterParameters({
location: {
pathParams: {
organization: MockDefaultOrganization.name,
groupName: MockGroupWithoutMembers.name,
},
},
routing: reactRouterOutlet(
{ path: "/organizations/:organization/groups/:groupName/settings" },
<GroupSettingsPage />,
),
}),
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
users: MockGroup.members,
count: MockGroup.members.length,
}),
permissionsQuery({ canUpdateGroup: false }),
],
},
play: async ({ canvasElement }) => {
const body = within(document.body);
await expect(
await body.findByText("You don't have permission to view this page"),
).toBeInTheDocument();
// The editable budget/name form is never rendered.
const canvas = within(canvasElement);
expect(
canvas.queryByRole("button", { name: "Save" }),
).not.toBeInTheDocument();
},
};

export const EveryoneGroup: Story = {
parameters: {
queries: [
Expand Down
11 changes: 9 additions & 2 deletions site/src/pages/GroupsPage/GroupSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Spinner } from "#/components/Spinner/Spinner";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { dollarsToMicros, microsToDollars } from "#/utils/currency";
import type { GroupPageOutletContext } from "./GroupPage";
import GroupSettingsPageView from "./GroupSettingsPageView";
Expand All @@ -23,20 +24,26 @@ const GroupSettingsPage: FC = () => {
organization?: string;
groupName: string;
};
const { group: groupData } = useOutletContext<GroupPageOutletContext>();
const { group: groupData, permissions } =
useOutletContext<GroupPageOutletContext>();
const queryClient = useQueryClient();
const patchGroupMutation = useMutation(patchGroup(queryClient, organization));
const navigate = useNavigate();

const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
const canUpdateGroup = permissions.canUpdateGroup;
const budgetQuery = useQuery({
...groupAIBudget(groupData.id),
enabled: aibridgeVisible,
enabled: aibridgeVisible && canUpdateGroup,
});
const saveBudgetMutation = useMutation(
saveGroupAIBudget(queryClient, groupData.id),
);

if (!canUpdateGroup) {
return <RequirePermission isFeatureVisible={false} />;
}

if (aibridgeVisible && budgetQuery.isLoading) {
return (
<div className="flex items-center justify-center p-10">
Expand Down
Loading