Files
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cef8610925 feat: add enabled column to UserFeatures and TeamFeatures for tri-state semantics (#25765)
* feat: add enabled column to UserFeatures and TeamFeatures for tri-state semantics

- Add enabled Boolean column to UserFeatures model with default true
- Add enabled Boolean column to TeamFeatures model with default true
- Update FeaturesRepository to use tri-state semantics:
  - enabled=true: feature is explicitly enabled
  - enabled=false: feature is explicitly disabled (blocks inheritance)
  - No row: inherit from team/org level
- Update SQL queries to check enabled=true for feature access
- Add enableFeatureForTeam method to interface and implementation

Co-Authored-By: [email protected] <[email protected]>

* update comments

* add integration tests

* add more test

* select enabled only

* no @default(true)

* fix types and tests

* add missing enabled

* add missing enabled

* rename enableFeatureForTeam to updateFeatureForTeam and support FeatureState

* refactor: rename updateFeatureForTeam to setTeamFeatureState

Co-Authored-By: [email protected] <[email protected]>

* fix integration test

* fix tests

* add more tests

* add missing enabled

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-18 07:16:20 -03:00

103 lines
4.2 KiB
TypeScript

import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import React from "react";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import type { TeamFeatures } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
import { Resource, CrudAction, CustomAction } from "@calcom/features/pbac/domain/types/permission-registry";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import type { SettingsLayoutProps } from "./SettingsLayoutAppDirClient";
import SettingsLayoutAppDirClient from "./SettingsLayoutAppDirClient";
const getTeamFeatures = unstable_cache(
async (teamId: number) => {
const featuresRepository = new FeaturesRepository(prisma);
return await featuresRepository.getEnabledTeamFeatures(teamId);
},
["team-features"],
{
revalidate: 120,
}
);
const getCachedResourcePermissions = unstable_cache(
async (userId: number, teamId: number, resource: Resource) => {
const permissionService = new PermissionCheckService();
return permissionService.getResourcePermissions({ userId, teamId, resource });
},
["resource-permissions"],
{ revalidate: 120 }
);
export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const userId = session?.user?.id;
if (!userId) {
return redirect("/auth/login");
}
let teamFeatures: Record<number, TeamFeatures> | null = null;
let canViewRoles = false;
let canViewOrganizationBilling = false;
let canUpdateOrganization = false;
let canViewAttributes = false;
const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
if (orgId) {
const isOrgAdminOrOwner = checkAdminOrOwner(session.user.org?.role);
const features = await getTeamFeatures(orgId);
if (features) {
teamFeatures = {
[orgId]: features,
};
}
// Check if PBAC feature is enabled
const isPbacEnabled = features?.pbac === true;
if (isPbacEnabled) {
// Only fetch and apply PBAC permissions if the feature is enabled
const [rolePermissions, organizationPermissions, attributesPermissions] = await Promise.all([
getCachedResourcePermissions(userId, orgId, Resource.Role),
getCachedResourcePermissions(userId, orgId, Resource.Organization),
getCachedResourcePermissions(userId, orgId, Resource.Attributes),
]);
// Check if user has permission to read roles
const roleActions = PermissionMapper.toActionMap(rolePermissions, Resource.Role);
canViewRoles = roleActions[CrudAction.Read] ?? false;
const orgActions = PermissionMapper.toActionMap(organizationPermissions, Resource.Organization);
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
canUpdateOrganization = orgActions[CrudAction.Update] ?? isOrgAdminOrOwner;
const attributesActions = PermissionMapper.toActionMap(attributesPermissions, Resource.Attributes);
canViewAttributes = attributesActions[CrudAction.Read] ?? isOrgAdminOrOwner;
} else {
// Fall back to legacy permissions when PBAC is not enabled or features not loaded
canViewRoles = features ? isOrgAdminOrOwner : false;
canViewOrganizationBilling = isOrgAdminOrOwner;
canUpdateOrganization = isOrgAdminOrOwner;
canViewAttributes = isOrgAdminOrOwner;
}
}
return (
<>
<SettingsLayoutAppDirClient
{...props}
teamFeatures={teamFeatures ?? {}}
permissions={{ canViewRoles, canViewOrganizationBilling, canUpdateOrganization, canViewAttributes }}
/>
</>
);
}