Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ae5ad4b
fix(app): use claude-sonnet-4.6 model
chasprowebdev Mar 12, 2026
c1194be
fix(app): upgrade the model in workflow visualizer file
chasprowebdev Mar 12, 2026
31fc265
fix(app): use gemini 3.1 flash lite with high reasoning
chasprowebdev Mar 13, 2026
3cf5fe4
feat(integration-platform): add Ramp role mapping functionality
tofikwest Mar 16, 2026
13e6446
feat(integration-platform): integrate logging for role mapping and sy…
tofikwest Mar 17, 2026
5d373ec
feat(integration-platform): validate connection existence in role map…
tofikwest Mar 17, 2026
433a1be
feat(integration-platform): enhance sync logging for role mapping con…
tofikwest Mar 17, 2026
1e24263
refactor(integration-platform): simplify role creation logic in RampR…
tofikwest Mar 17, 2026
bca0ca7
Merge branch 'main' into tofik/ramp-integration
tofikwest Mar 17, 2026
6105a67
fix(integration-platform): improve error handling in RampRoleMappingC…
tofikwest Mar 17, 2026
e0a434c
Merge branch 'tofik/ramp-integration' of github.com:trycompai/comp in…
tofikwest Mar 17, 2026
7e52ec4
feat(integration-platform): enhance role mapping persistence logic
tofikwest Mar 17, 2026
0e34a8e
chore: merge release v3.7.2 back to main [skip ci]
github-actions[bot] Mar 17, 2026
638e670
feat(integration-platform): implement RampApiService for user management
tofikwest Mar 17, 2026
06b9326
Merge branch 'main' into tofik/ramp-integration
tofikwest Mar 17, 2026
71e8aa7
Merge pull request #2312 from trycompai/tofik/ramp-integration
tofikwest Mar 17, 2026
0659828
Merge branch 'main' into chas/upgrade-ai-model
tofikwest Mar 17, 2026
10f84a0
Merge pull request #2294 from trycompai/chas/upgrade-ai-model
tofikwest Mar 17, 2026
d5915ca
fix(api): make sure azure assessmet tile is not empty
chasprowebdev Mar 17, 2026
37dc0a4
Merge pull request #2323 from trycompai/chas/azure-cloud-tests
tofikwest Mar 17, 2026
7f873aa
feat: improve AI policy editor, better UI/UX and smarter
github-actions[bot] Mar 17, 2026
2ac265b
fix: fix inline editing for sections in ai policy editor
github-actions[bot] Mar 17, 2026
f85e757
feat(policy-editor): add inline AI text editing via selection bubble …
Marfuen Mar 17, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ node_modules
.pnp.js

.idea/
.superpowers/
docs/superpowers/
# testing
coverage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ export class AzureSecurityService {
for (const assessment of unhealthy.slice(0, 50)) {
findings.push({
id: assessment.name,
title: assessment.properties.displayName,
title:
assessment.properties.displayName ||
assessment.name ||
'Unhealthy security assessment',
description:
assessment.properties.metadata?.description ||
assessment.properties.status.description ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import {
Controller,
Post,
Get,
Query,
Body,
HttpException,
HttpStatus,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiSecurity } from '@nestjs/swagger';
import { HybridAuthGuard } from '../../auth/hybrid-auth.guard';
import { PermissionGuard } from '../../auth/permission.guard';
import { RequirePermission } from '../../auth/require-permission.decorator';
import { OrganizationId } from '../../auth/auth-context.decorator';
import { db } from '@db';
import { ConnectionRepository } from '../repositories/connection.repository';
import { RampRoleMappingService } from '../services/ramp-role-mapping.service';
import { IntegrationSyncLoggerService } from '../services/integration-sync-logger.service';
import { RampApiService } from '../services/ramp-api.service';
import { type RoleMappingEntry } from '@trycompai/integration-platform';

@Controller({ path: 'integrations/sync/ramp', version: '1' })
@ApiTags('Integrations')
@UseGuards(HybridAuthGuard, PermissionGuard)
@ApiSecurity('apikey')
export class RampRoleMappingController {
constructor(
private readonly connectionRepository: ConnectionRepository,
private readonly roleMappingService: RampRoleMappingService,
private readonly syncLoggerService: IntegrationSyncLoggerService,
private readonly rampApiService: RampApiService,
) {}

@Post('discover-roles')
@RequirePermission('integration', 'update')
async discoverRoles(
@OrganizationId() organizationId: string,
@Query('connectionId') connectionId: string,
@Query('refresh') refresh?: string,
) {
if (!connectionId) {
throw new HttpException('connectionId is required', HttpStatus.BAD_REQUEST);
}

const connection = await this.connectionRepository.findById(connectionId);
if (!connection || connection.organizationId !== organizationId) {
throw new HttpException('Connection not found', HttpStatus.NOT_FOUND);
}

const shouldRefresh = refresh === 'true';
let discoveredRoles: Array<{ role: string; userCount: number }>;

// Use cached roles unless refresh is requested
const cachedRoles = shouldRefresh
? null
: await this.roleMappingService.getCachedDiscoveredRoles(connectionId);

if (cachedRoles) {
discoveredRoles = cachedRoles;
} else {
const logId = await this.syncLoggerService.startLog({
connectionId,
organizationId,
provider: 'ramp',
eventType: 'role_discovery',
triggeredBy: 'manual',
});

try {
const accessToken = await this.rampApiService.getAccessToken(connectionId, organizationId);
const users = await this.rampApiService.fetchUsers(accessToken);

const roleCounts = new Map<string, number>();
for (const user of users) {
const role = user.role ?? 'UNKNOWN';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role Discovery / Sync Mismatch for Roleless Users

Medium Severity

In discoverRoles, Ramp users whose role is null/undefined are counted under the synthetic key 'UNKNOWN' — so 'UNKNOWN' appears in the UI as a configurable role. During actual sync in syncRampEmployeesInner, those same users are looked up in roleMappingLookup with the key '' (empty string) because rampUser.role ?? '' is used. Since no mapping entry has the key '', the lookup always falls back to 'employee', silently ignoring whatever the user mapped 'UNKNOWN' to.

Additional Locations (1)
Fix in Cursor Fix in Web

roleCounts.set(role, (roleCounts.get(role) ?? 0) + 1);
}

discoveredRoles = Array.from(roleCounts.entries())
.map(([role, userCount]) => ({ role, userCount }))
.sort((a, b) => b.userCount - a.userCount);

// Cache the discovered roles (preserve existing mapping if any)
const existingMapping = await this.roleMappingService.getSavedMapping(connectionId);
if (existingMapping) {
await this.roleMappingService.saveMapping(
connectionId,
existingMapping,
discoveredRoles,
);
} else {
await this.roleMappingService.saveDiscoveredRoles(connectionId, discoveredRoles);
}

await this.syncLoggerService.completeLog(logId, {
rolesDiscovered: discoveredRoles.length,
totalUsers: users.length,
});
} catch (error) {
await this.syncLoggerService.failLog(
logId,
error instanceof Error ? error.message : String(error),
);
throw error;
}
}

const rampRoleNames = discoveredRoles.map((r) => r.role);
const defaultMapping = this.roleMappingService.getDefaultMapping(rampRoleNames);
const existingMapping = await this.roleMappingService.getSavedMapping(connectionId);

// Fetch existing custom roles for this org with their permissions
const customRoles = await db.organizationRole.findMany({
where: { organizationId },
select: { name: true, permissions: true, obligations: true },
orderBy: { name: 'asc' },
});

const existingCustomRoles = customRoles.map((r) => ({
name: r.name,
permissions: JSON.parse(r.permissions) as Record<string, string[]>,
obligations: JSON.parse(r.obligations) as Record<string, boolean>,
}));

return { discoveredRoles, defaultMapping, existingMapping, existingCustomRoles };
}

@Post('role-mapping')
@RequirePermission('integration', 'update')
async saveRoleMapping(
@OrganizationId() organizationId: string,
@Body() body: { connectionId: string; mapping: RoleMappingEntry[] },
) {
const { connectionId, mapping } = body;

if (!connectionId || !Array.isArray(mapping)) {
throw new HttpException(
'connectionId and mapping are required',
HttpStatus.BAD_REQUEST,
);
}

const connection = await this.connectionRepository.findById(connectionId);
if (!connection || connection.organizationId !== organizationId) {
throw new HttpException('Connection not found', HttpStatus.NOT_FOUND);
}

const logId = await this.syncLoggerService.startLog({
connectionId,
organizationId,
provider: 'ramp',
eventType: 'role_mapping_save',
triggeredBy: 'manual',
});

try {
// Create custom roles in the database
await this.roleMappingService.ensureCustomRolesExist(organizationId, mapping);

// Save mapping to connection variables (preserve existing discovered roles)
await this.roleMappingService.saveMapping(connectionId, mapping);

await this.syncLoggerService.completeLog(logId, {
mappingCount: mapping.length,
});

return { success: true, mapping };
} catch (error) {
await this.syncLoggerService.failLog(
logId,
error instanceof Error ? error.message : String(error),
);
throw error;
}
}

@Get('role-mapping')
@RequirePermission('integration', 'read')
async getRoleMapping(
@OrganizationId() organizationId: string,
@Query('connectionId') connectionId: string,
) {
if (!connectionId) {
throw new HttpException('connectionId is required', HttpStatus.BAD_REQUEST);
}

const connection = await this.connectionRepository.findById(connectionId);
if (!connection || connection.organizationId !== organizationId) {
throw new HttpException('Connection not found', HttpStatus.NOT_FOUND);
}

const mapping = await this.roleMappingService.getSavedMapping(connectionId);
return { mapping };
}
}
Loading
Loading