Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 12 additions & 1 deletion controlplane/src/core/bufservices/api-key/createAPIKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ApiKeyGenerator } from '../../services/ApiGenerator.js';
import { enrichLogger, getLogger, handleError } from '../../util.js';
import { OrganizationGroupRepository } from '../../repositories/OrganizationGroupRepository.js';
import { UnauthorizedError } from '../../errors/errors.js';
import { RBACEvaluator } from '../../services/RBACEvaluator.js';

export function createAPIKey(
opts: RouterOptions,
Expand Down Expand Up @@ -70,8 +71,18 @@ export function createAPIKey(
};
}

const generatedAPIKey = ApiKeyGenerator.generate();
const rbac = new RBACEvaluator([orgGroup]);
if (rbac.isOrganizationAdmin && !authContext.rbac.isOrganizationAdmin) {
return {
response: {
code: EnumStatusCode.ERR,
details: "You don't have the required permissions to assign this group",
},
apiKey: '',
};
}

const generatedAPIKey = ApiKeyGenerator.generate();
await apiKeyRepo.addAPIKey({
name: keyName,
organizationID: authContext.organizationId,
Expand Down
13 changes: 12 additions & 1 deletion controlplane/src/core/bufservices/api-key/updateAPIKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { RouterOptions } from '../../routes.js';
import { enrichLogger, getLogger, handleError } from '../../util.js';
import { OrganizationGroupRepository } from '../../repositories/OrganizationGroupRepository.js';
import { UnauthorizedError } from '../../errors/errors.js';
import { RBACEvaluator } from '../../services/RBACEvaluator.js';

export function updateAPIKey(
opts: RouterOptions,
Expand Down Expand Up @@ -56,8 +57,18 @@ export function updateAPIKey(
};
}

await apiKeyRepo.updateAPIKeyGroup({ apiKeyId: apiKey.id, groupId: orgGroup.groupId });
const rbac = new RBACEvaluator([orgGroup]);
if (rbac.isOrganizationAdmin && !authContext.rbac.isOrganizationAdmin) {
return {
response: {
code: EnumStatusCode.ERR,
details: "You don't have the required permissions to assign this group",
},
apiKey: '',
};
}

await apiKeyRepo.updateAPIKeyGroup({ apiKeyId: apiKey.id, groupId: orgGroup.groupId });
await auditLogRepo.addAuditLog({
organizationId: authContext.organizationId,
organizationSlug: authContext.organizationSlug,
Expand Down
73 changes: 68 additions & 5 deletions controlplane/test/api-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,20 @@ describe('API Keys', (ctx) => {
const orgGroupsResponse = await client.getOrganizationGroups({});
expect(orgGroupsResponse.response?.code).toBe(EnumStatusCode.OK);

const adminGroup = orgGroupsResponse.groups.find((g) => g.name === 'admin')!;
const developerGroup = orgGroupsResponse.groups.find((g) => g.name === 'developer')!;
const viewerGroup = orgGroupsResponse.groups.find((g) => g.name === 'viewer')!;

authenticator.changeUserWithSuppliedContext({
...users[TestUser.adminAliceCompanyA],
rbac: createTestRBACEvaluator(createTestGroup({ role: role as OrganizationRole })),
});

// Create the API key with the `admin` group
// Create the API key with the `viewer` group
const apiKeyName = uid();
const createApiKeyResponse = await client.createAPIKey({
name: apiKeyName,
expires: ExpiresAt.NEVER,
groupId: adminGroup.groupId,
groupId: viewerGroup.groupId,
});

expect(createApiKeyResponse.response?.code).toBe(EnumStatusCode.OK);
Expand All @@ -136,7 +136,7 @@ describe('API Keys', (ctx) => {

expect(updateApiKeyResponse.response?.code).toBe(EnumStatusCode.OK);

// Ensure that the API key have the correct group
// Ensure that the API key has the correct group
let getApiKeysResponse = await client.getAPIKeys({});
let apiKey = getApiKeysResponse.apiKeys?.find((k) => k.name === apiKeyName);

Expand All @@ -150,7 +150,7 @@ describe('API Keys', (ctx) => {

expect(deleteApiKeyResponse.response?.code).toBe(EnumStatusCode.OK);

// Ensure the API key have been deleted
// Ensure the API key has been deleted
getApiKeysResponse = await client.getAPIKeys({});
apiKey = getApiKeysResponse.apiKeys?.find((k) => k.name === apiKeyName);

Expand All @@ -160,6 +160,69 @@ describe('API Keys', (ctx) => {
await server.close();
});

test('that an "organization-apikey-manager" cannot create API keys with admin role', async () => {
const { client, server, users, authenticator } = await SetupTest({ dbname, enableMultiUsers: true });

const orgGroupsResponse = await client.getOrganizationGroups({});
expect(orgGroupsResponse.response?.code).toBe(EnumStatusCode.OK);

const adminGroup = orgGroupsResponse.groups.find((g) => g.name === 'admin')!;

authenticator.changeUserWithSuppliedContext({
...users[TestUser.adminAliceCompanyA],
rbac: createTestRBACEvaluator(createTestGroup({ role: 'organization-apikey-manager' })),
});

// Create the API key with the `admin` group
const apiKeyName = uid();
const createApiKeyResponse = await client.createAPIKey({
name: apiKeyName,
expires: ExpiresAt.NEVER,
groupId: adminGroup.groupId,
});

expect(createApiKeyResponse.response?.code).toBe(EnumStatusCode.ERR);
expect(createApiKeyResponse.response?.details).toBe("You don't have the required permissions to assign this group");

await server.close();
});

test('that an "organization-apikey-manager" cannot update API keys with admin role', async () => {
const { client, server, users, authenticator } = await SetupTest({ dbname, enableMultiUsers: true });

const orgGroupsResponse = await client.getOrganizationGroups({});
expect(orgGroupsResponse.response?.code).toBe(EnumStatusCode.OK);

const adminGroup = orgGroupsResponse.groups.find((g) => g.name === 'admin')!;
const viewerGroup = orgGroupsResponse.groups.find((g) => g.name === 'viewer')!;

authenticator.changeUserWithSuppliedContext({
...users[TestUser.adminAliceCompanyA],
rbac: createTestRBACEvaluator(createTestGroup({ role: 'organization-apikey-manager' })),
});

// Create the API key with the `viewer` group
const apiKeyName = uid();
const createApiKeyResponse = await client.createAPIKey({
name: apiKeyName,
expires: ExpiresAt.NEVER,
groupId: viewerGroup.groupId,
});

expect(createApiKeyResponse.response?.code).toBe(EnumStatusCode.OK);

// Update the API key to the `admin` group
const updateApiKeyResponse = await client.updateAPIKey({
name: apiKeyName,
groupId: adminGroup.groupId,
});

expect(updateApiKeyResponse.response?.code).toBe(EnumStatusCode.ERR);
expect(updateApiKeyResponse.response?.details).toBe("You don't have the required permissions to assign this group");

await server.close();
});

test.each([
'organization-developer',
'organization-viewer',
Expand Down
3 changes: 3 additions & 0 deletions studio/src/components/group-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery } from "@connectrpc/connect-query";
import { getOrganizationGroups } from "@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery";
import { EnumStatusCode } from "@wundergraph/cosmo-connect/dist/common/common_pb";
import { Button } from "@/components/ui/button";
import { useIsAdmin } from "@/hooks/use-is-admin";

export function GroupSelect({ id, value, disabled = false, groups, onValueChange }: {
id?: string;
Expand All @@ -12,6 +13,7 @@ export function GroupSelect({ id, value, disabled = false, groups, onValueChange
groups?: OrganizationGroup[];
onValueChange(group: OrganizationGroup): void;
}) {
const isAdmin = false;// useIsAdmin();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const { data, isPending, error, refetch } = useQuery(getOrganizationGroups, {}, { enabled: groups === undefined });
if (isPending) {
return (
Expand Down Expand Up @@ -60,6 +62,7 @@ export function GroupSelect({ id, value, disabled = false, groups, onValueChange
<SelectItem
key={`group-${group.groupId}`}
value={group.groupId}
disabled={!isAdmin && group.rules.some((rule) => rule.role === 'organization-admin')}
>
{group.name}
</SelectItem>
Expand Down
2 changes: 1 addition & 1 deletion studio/src/pages/[organizationSlug]/members.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ const InviteForm = ({ onSuccess }: { onSuccess: () => void }) => {
</div>

<div className="space-y-2">
<span>What group should the member be added to?</span>
<span>What group(s) should the member be added to?</span>
<MultiGroupSelect
disabled={isPending}
value={selectedGroups}
Expand Down
Loading