Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: launch m2m app for organizations #6129

Merged
merged 2 commits into from
Jul 1, 2024
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
29 changes: 29 additions & 0 deletions .changeset/gentle-camels-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@logto/console": minor
"@logto/core": minor
"@logto/phrases": minor
"@logto/schemas": minor
"@logto/integration-tests": patch
---

support machine-to-machine apps for organizations

This feature allows machine-to-machine apps to be associated with organizations, and be assigned with organization roles.

### Console

- Add a new "machine-to-machine" type to organization roles. All existing roles are now "user" type.
- You can manage machine-to-machine apps in the organization details page -> Machine-to-machine apps section.
- You can view the associated organizations in the machine-to-machine app details page.

### OpenID Connect grant

The `client_credentials` grant type is now supported for organizations. You can use this grant type to obtain an access token for an organization.

### Management API

A set of new endpoints are added to the Management API:

- `/api/organizations/{id}/applications` to manage machine-to-machine apps.
- `/api/organizations/{id}/applications/{applicationId}` to manage a specific machine-to-machine app in an organization.
- `/api/applications/{id}/organizations` to view the associated organizations of a machine-to-machine app.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { condArray } from '@silverhand/essentials';
import { Navigate, type RouteObject } from 'react-router-dom';

import { isDevFeaturesEnabled } from '@/consts/env';
import OrganizationDetails from '@/pages/OrganizationDetails';
import MachineToMachine from '@/pages/OrganizationDetails/MachineToMachine';
import Members from '@/pages/OrganizationDetails/Members';
Expand All @@ -17,15 +16,15 @@ export const organizations: RouteObject = {
{
path: ':id/*',
element: <OrganizationDetails />,
children: condArray(
children: [
{ index: true, element: <Navigate replace to={OrganizationDetailsTabs.Settings} /> },
{ path: OrganizationDetailsTabs.Settings, element: <Settings /> },
{ path: OrganizationDetailsTabs.Members, element: <Members /> },
isDevFeaturesEnabled && {
{
path: OrganizationDetailsTabs.MachineToMachine,
element: <MachineToMachine />,
}
),
},
],
}
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import OrganizationList from '@/components/OrganizationList';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { ApplicationDetailsTabs, logtoThirdPartyGuideLink, protectedAppLink } from '@/consts';
import { isDevFeaturesEnabled } from '@/consts/env';
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import TabWrapper from '@/ds-components/TabWrapper';
Expand Down Expand Up @@ -178,11 +177,9 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
<TabNavItem href={`/applications/${data.id}/${ApplicationDetailsTabs.Logs}`}>
{t('application_details.machine_logs')}
</TabNavItem>
{isDevFeaturesEnabled && (
<TabNavItem href={`/applications/${data.id}/${ApplicationDetailsTabs.Organizations}`}>
{t('organizations.title')}
</TabNavItem>
)}
<TabNavItem href={`/applications/${data.id}/${ApplicationDetailsTabs.Organizations}`}>
{t('organizations.title')}
</TabNavItem>
</>
)}
{data.isThirdParty && (
Expand Down
11 changes: 4 additions & 7 deletions packages/console/src/pages/OrganizationDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import Skeleton from '@/components/DetailsPage/Skeleton';
import Drawer from '@/components/Drawer';
import PageMeta from '@/components/PageMeta';
import ThemedIcon from '@/components/ThemedIcon';
import { isDevFeaturesEnabled } from '@/consts/env';
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import useApi, { type RequestError } from '@/hooks/use-api';
Expand Down Expand Up @@ -134,12 +133,10 @@ function OrganizationDetails() {
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.Members}`}>
{t('organizations.members')}
</TabNavItem>
{/* TODO: Remove */}
{isDevFeaturesEnabled && (
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.MachineToMachine}`}>
{t('organizations.machine_to_machine')}
</TabNavItem>
)}

<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.MachineToMachine}`}>
{t('organizations.machine_to_machine')}
</TabNavItem>
</TabNav>
<Outlet
context={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';

import { isDevFeaturesEnabled } from '@/consts/env';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
Expand Down Expand Up @@ -105,29 +104,26 @@ function CreateOrganizationRoleModal({ isOpen, onClose }: Props) {
{...register('description')}
/>
</FormField>
{/* TODO: Remove */}
{isDevFeaturesEnabled && (
<FormField title="organization_template.roles.create_modal.type">
<Controller
name="type"
control={control}
render={({ field: { onChange, value, name } }) => (
<RadioGroup
name={name}
className={styles.roleTypes}
value={value}
onChange={(value) => {
onChange(value);
}}
>
{radioOptions.map(({ key, value }) => (
<Radio key={value} title={<DynamicT forKey={key} />} value={value} />
))}
</RadioGroup>
)}
/>
</FormField>
)}
<FormField title="organization_template.roles.create_modal.type">
<Controller
name="type"
control={control}
render={({ field: { onChange, value, name } }) => (
<RadioGroup
name={name}
className={styles.roleTypes}
value={value}
onChange={(value) => {
onChange(value);
}}
>
{radioOptions.map(({ key, value }) => (
<Radio key={value} title={<DynamicT forKey={key} />} value={value} />
))}
</RadioGroup>
)}
/>
</FormField>
</ModalLayout>
</ReactModal>
);
Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/oidc/grants/client-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import dpopValidate from 'oidc-provider/lib/helpers/validate_dpop.js';
import instance from 'oidc-provider/lib/helpers/weak_cache.js';
import checkResource from 'oidc-provider/lib/shared/check_resource.js';

import { EnvSet } from '#src/env-set/index.js';
import { type EnvSet } from '#src/env-set/index.js';
import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';

Expand Down Expand Up @@ -68,10 +68,6 @@ export const buildHandler: (
// The value type is `unknown`, which will swallow other type inferences. So we have to cast it
// to `Boolean` first.
const organizationId = cond(Boolean(params?.organization_id) && String(params?.organization_id));
// TODO: Remove
if (!EnvSet.values.isDevFeaturesEnabled && organizationId) {
throw new InvalidTarget('organization tokens are not supported yet');
}

if (
organizationId &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"paths": {
"/api/applications/{id}/organizations": {
"get": {
"tags": ["Dev feature"],
"summary": "Get application organizations",
"description": "Get the list of organizations that an application is associated with.",
"responses": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { organizationWithOrganizationRolesGuard } from '@logto/schemas';
import { z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';

Expand All @@ -10,11 +9,6 @@ import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
export default function applicationOrganizationRoutes<T extends ManagementApiRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
// TODO: Remove
if (!EnvSet.values.isDevFeaturesEnabled) {
return;
}

router.get(
'/applications/:id/organizations',
koaPagination({ isOptional: true }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
{
"name": "Organization applications",
"description": "Manage organization - application relationships. An application can be associated with one or more organizations in order to get access to the organization resources.\n\nCurrently, only machine-to-machine applications can be associated with organizations."
},
{ "name": "Dev feature" }
}
],
"paths": {
"/api/organizations/{id}/applications": {
Expand Down Expand Up @@ -81,7 +80,6 @@
},
"/api/organizations/{id}/applications/roles": {
"post": {
"tags": ["Dev feature"],
"summary": "Assign roles to applications in an organization",
"description": "Assign roles to applications in the specified organization.",
"requestBody": {
Expand Down
111 changes: 54 additions & 57 deletions packages/core/src/routes/organization/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
} from '@logto/schemas';
import { z } from 'zod';

import { EnvSet } from '#src/env-set/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import { applicationSearchKeys } from '#src/queries/application.js';
Expand All @@ -21,69 +20,67 @@
router: SchemaRouter<OrganizationKeys, CreateOrganization, Organization>,
organizations: OrganizationQueries
) {
if (EnvSet.values.isDevFeaturesEnabled) {
// MARK: Organization - application relation routes
router.addRelationRoutes(organizations.relations.apps, undefined, {
disabled: { get: true },
hookEvent: 'Organization.Membership.Updated',
});
// MARK: Organization - application relation routes
router.addRelationRoutes(organizations.relations.apps, undefined, {
disabled: { get: true },
hookEvent: 'Organization.Membership.Updated',
});

router.get(
'/:id/applications',
koaPagination(),
koaGuard({
query: z.object({ q: z.string().optional() }),
params: z.object({ id: z.string().min(1) }),
response: applicationWithOrganizationRolesGuard.array(),
status: [200, 404],
}),
async (ctx, next) => {
const search = parseSearchOptions(applicationSearchKeys, ctx.guard.query);
router.get(
'/:id/applications',
koaPagination(),
koaGuard({
query: z.object({ q: z.string().optional() }),
params: z.object({ id: z.string().min(1) }),
response: applicationWithOrganizationRolesGuard.array(),
status: [200, 404],
}),
async (ctx, next) => {
const search = parseSearchOptions(applicationSearchKeys, ctx.guard.query);

Check warning on line 39 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L39

Added line #L39 was not covered by tests

const [totalCount, entities] =
await organizations.relations.apps.getApplicationsByOrganizationId(
ctx.guard.params.id,
ctx.pagination,
search
);
const [totalCount, entities] =
await organizations.relations.apps.getApplicationsByOrganizationId(
ctx.guard.params.id,
ctx.pagination,
search
);

Check warning on line 46 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L41-L46

Added lines #L41 - L46 were not covered by tests

ctx.pagination.totalCount = totalCount;
ctx.body = entities;
ctx.pagination.totalCount = totalCount;
ctx.body = entities;

Check warning on line 49 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L48-L49

Added lines #L48 - L49 were not covered by tests

return next();
}
);
return next();
}

Check warning on line 52 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L51-L52

Added lines #L51 - L52 were not covered by tests
);

router.post(
'/:id/applications/roles',
koaGuard({
params: z.object({ id: z.string().min(1) }),
body: z.object({
applicationIds: z.string().min(1).array().nonempty(),
organizationRoleIds: z.string().min(1).array().nonempty(),
}),
status: [201, 422],
router.post(
'/:id/applications/roles',
koaGuard({
params: z.object({ id: z.string().min(1) }),
body: z.object({
applicationIds: z.string().min(1).array().nonempty(),
organizationRoleIds: z.string().min(1).array().nonempty(),
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { applicationIds, organizationRoleIds } = ctx.guard.body;
status: [201, 422],
}),
async (ctx, next) => {
const { id } = ctx.guard.params;
const { applicationIds, organizationRoleIds } = ctx.guard.body;

Check warning on line 67 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L66-L67

Added lines #L66 - L67 were not covered by tests

await organizations.relations.appsRoles.insert(
...organizationRoleIds.flatMap((organizationRoleId) =>
applicationIds.map((applicationId) => ({
organizationId: id,
applicationId,
organizationRoleId,
}))
)
);
await organizations.relations.appsRoles.insert(
...organizationRoleIds.flatMap((organizationRoleId) =>
applicationIds.map((applicationId) => ({
organizationId: id,
applicationId,
organizationRoleId,
}))
)
);

Check warning on line 77 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L69-L77

Added lines #L69 - L77 were not covered by tests

ctx.status = 201;
return next();
}
);
ctx.status = 201;
return next();
}

Check warning on line 81 in packages/core/src/routes/organization/application/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/organization/application/index.ts#L79-L81

Added lines #L79 - L81 were not covered by tests
);

// MARK: Organization - application role relation routes
applicationRoleRelationRoutes(router, organizations);
}
// MARK: Organization - application role relation routes
applicationRoleRelationRoutes(router, organizations);
}
2 changes: 1 addition & 1 deletion packages/core/src/routes/swagger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const identifiableEntityNames = Object.freeze([
/** Additional tags that cannot be inferred from the path. */
const additionalTags = Object.freeze(
condArray<string>(
EnvSet.values.isDevFeaturesEnabled && 'Organization applications',
'Organization applications',
EnvSet.values.isDevFeaturesEnabled && 'Security',
'Organization users'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
getOrganizations,
} from '#src/api/application.js';
import { OrganizationApiTest } from '#src/helpers/organization.js';
import { devFeatureTest, generateTestName } from '#src/utils.js';
import { generateTestName } from '#src/utils.js';

devFeatureTest.describe('application organizations', () => {
describe('application organizations', () => {
const organizationApi = new OrganizationApiTest();
const applications: Application[] = [];
const createApplication = async (...args: Parameters<typeof createApplicationApi>) => {
Expand Down
Loading
Loading