Skip to content

Commit 7b9129c

Browse files
authored
feat(app): organization access token management ui (#6556)
1 parent c622471 commit 7b9129c

32 files changed

+2567
-480
lines changed

.changeset/neat-ladybugs-pay.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'hive': minor
3+
---
4+
5+
Add organization access tokens; a new way to issue access tokens for performing actions with the CLI
6+
and doing usage reporting.
7+
8+
**Breaking Change:** The `usage` service now requires environment variables for Postgres
9+
(`POSTGRES_SSL`, `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`,
10+
`POSTGRES_PASSWORD`) and Redis (`REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD`, `REDIS_TLS_ENABLED`).
11+
12+
For more information please refer to the organization access token documentation.
13+
14+
- [Product Update: Organization-Level Access Tokens for Enhanced Security & Flexibility](https://the-guild.dev/graphql/hive/product-updates/2025-03-10-new-access-tokens)
15+
- [Migration Guide: Moving from Registry Access Tokens to Access Tokens](https://the-guild.dev/graphql/hive/docs/migration-guides/organization-access-tokens)
16+
- [Access Token Documentation](https://the-guild.dev/graphql/hive/docs/management/access-tokens)

integration-tests/tests/api/organization-access-tokens.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ test.concurrent('create: failure invalid title', async ({ expect }) => {
129129
{
130130
details: {
131131
description: null,
132-
title: Can only contain letters, numbers, " ", '_', and '-'.,
132+
title: Can only contain letters, numbers, " ", "_", and "-".,
133133
},
134134
message: Invalid input provided.,
135135
}

integration-tests/tests/api/organization/members.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ test.concurrent('owner of an organization should have all scopes', async ({ expe
1111
[
1212
organization:describe,
1313
support:manageTickets,
14+
accessToken:modify,
1415
organization:modifySlug,
1516
auditLog:export,
1617
organization:delete,

packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ export const permissionGroups: Array<PermissionGroup> = [
1818
permissions: [
1919
{
2020
id: 'project:describe',
21-
title: 'View project',
21+
title: 'Describe project',
2222
description: 'Fetch information about the specified projects.',
2323
},
2424
],
2525
},
2626
{
27-
id: 'targets',
28-
title: 'Targets',
27+
id: 'usage-reporting',
28+
title: 'Usage Reporting',
2929
permissions: [
3030
{
3131
id: 'usage:report',

packages/services/api/src/modules/organization/lib/organization-member-permissions.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export const permissionGroups: Array<PermissionGroup> = [
1717
title: 'Access support tickets',
1818
description: 'Member can access, create and reply to support tickets.',
1919
},
20+
{
21+
id: 'accessToken:modify',
22+
title: 'Manage organization access tokens',
23+
description: 'Member can create and delete organization access tokens.',
24+
warning:
25+
'Granting a role the ability to manage members enables it to elevate its own permissions.',
26+
},
2027
{
2128
id: 'organization:modifySlug',
2229
title: 'Update organization slug',
@@ -266,7 +273,6 @@ assertAllRulesAreAssigned([
266273
'appDeployment:publish',
267274
'appDeployment:retire',
268275
'usage:report',
269-
'accessToken:modify',
270276
]);
271277

272278
/**

packages/services/api/src/modules/organization/module.graphql.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default gql`
8888
description: String
8989
permissions: [String!]!
9090
resources: ResourceAssignment!
91+
firstCharacters: String!
9192
createdAt: DateTime!
9293
}
9394
@@ -320,9 +321,17 @@ export default gql`
320321
"""
321322
availableOrganizationPermissionGroups: [PermissionGroup!]!
322323
"""
324+
Whether the viewer can manage access tokens.
325+
"""
326+
viewerCanManageAccessTokens: Boolean!
327+
"""
323328
Paginated organization access tokens.
324329
"""
325330
accessTokens(first: Int, after: String): OrganizationAccessTokenConnection!
331+
"""
332+
Get organization access token by id.
333+
"""
334+
accessToken(id: ID!): OrganizationAccessToken
326335
}
327336
328337
type OrganizationAccessTokenEdge {

packages/services/api/src/modules/organization/providers/organization-access-tokens.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
const TitleInputModel = z
3232
.string()
3333
.trim()
34-
.regex(/^[ a-zA-Z0-9_-]+$/, `Can only contain letters, numbers, " ", '_', and '-'.`)
34+
.regex(/^[ a-zA-Z0-9_-]+$/, 'Can only contain letters, numbers, " ", "_", and "-".')
3535
.min(2, 'Minimum length is 2 characters.')
3636
.max(100, 'Maximum length is 100 characters.');
3737

@@ -305,6 +305,30 @@ export class OrganizationAccessTokens {
305305
},
306306
};
307307
}
308+
309+
async get(args: { organizationId: string; id: string }) {
310+
await this.session.assertPerformAction({
311+
organizationId: args.organizationId,
312+
params: { organizationId: args.organizationId },
313+
action: 'accessToken:modify',
314+
});
315+
316+
const row = await this.pool.maybeOne<unknown>(sql` /* OrganizationAccessTokens.getPaginated */
317+
SELECT
318+
${organizationAccessTokenFields}
319+
FROM
320+
"organization_access_tokens"
321+
WHERE
322+
"id" = ${args.id}
323+
AND "organization_id" = ${args.organizationId}
324+
`);
325+
326+
if (row === null) {
327+
return null;
328+
}
329+
330+
return OrganizationAccessTokenModel.parse(row);
331+
}
308332
}
309333

310334
/**

packages/services/api/src/modules/organization/resolvers/Organization.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { OrganizationResolvers } from './../../../__generated__/types';
99

1010
export const Organization: Pick<
1111
OrganizationResolvers,
12+
| 'accessToken'
1213
| 'accessTokens'
1314
| 'availableMemberPermissionGroups'
1415
| 'availableOrganizationPermissionGroups'
@@ -26,6 +27,7 @@ export const Organization: Pick<
2627
| 'viewerCanAssignUserRoles'
2728
| 'viewerCanDelete'
2829
| 'viewerCanExportAuditLogs'
30+
| 'viewerCanManageAccessTokens'
2931
| 'viewerCanManageInvitations'
3032
| 'viewerCanManageRoles'
3133
| 'viewerCanModifySlug'
@@ -142,6 +144,13 @@ export const Organization: Pick<
142144
organizationId: organization.id,
143145
},
144146
}),
147+
session.canPerformAction({
148+
action: 'accessToken:modify',
149+
organizationId: organization.id,
150+
params: {
151+
organizationId: organization.id,
152+
},
153+
}),
145154
]).then(result => result.some(Boolean));
146155
},
147156
viewerCanSeeMembers: async (organization, _arg, { session }) => {
@@ -203,4 +212,19 @@ export const Organization: Pick<
203212
after: args.after ?? null,
204213
});
205214
},
215+
viewerCanManageAccessTokens: async (organization, _arg, { session }) => {
216+
return session.canPerformAction({
217+
organizationId: organization.id,
218+
action: 'accessToken:modify',
219+
params: {
220+
organizationId: organization.id,
221+
},
222+
});
223+
},
224+
accessToken: async (organization, args, { injector }) => {
225+
return injector.get(OrganizationAccessTokens).get({
226+
organizationId: organization.id,
227+
id: args.id,
228+
});
229+
},
206230
};

packages/services/storage/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5207,7 +5207,6 @@ export function findTargetBySlug(deps: { pool: DatabasePool }) {
52075207
return null;
52085208
}
52095209

5210-
// Consider adding error handling similar to what was suggested for findTargetById.
52115210
return TargetWithOrgIdModel.parse(data);
52125211
};
52135212
}

packages/web/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@sentry/node": "7.120.2",
5353
"@sentry/react": "7.120.2",
5454
"@sentry/types": "7.120.2",
55+
"@stepperize/react": "5.1.0",
5556
"@storybook/addon-essentials": "8.4.7",
5657
"@storybook/addon-interactions": "8.4.7",
5758
"@storybook/addon-links": "8.4.7",

0 commit comments

Comments
 (0)