Skip to content

Commit

Permalink
feat(rbac): add audit logging to connect provider
Browse files Browse the repository at this point in the history
  • Loading branch information
PatAKnight committed Aug 13, 2024
1 parent a5b95cb commit ef84e2c
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 66 deletions.
6 changes: 3 additions & 3 deletions plugins/rbac-backend-module-test/test-policy.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
g, user:default/test-provider, role:default/test-provider
g, user:default/test-provider-three, role:default/test-provider-two
g, user:default/test-provider-two, role:default/test-provider-two


p, role:default/test-role, catalog-entity, delete, allow
p, role:default/group-1g, catalog.entity.create, create, allow
p, role:default/test-provider, catalog-entity, delete, allow
p, role:default/test-provider-two, catalog.entity.create, create, allow
213 changes: 159 additions & 54 deletions plugins/rbac-backend/src/providers/connect-providers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { LoggerService } from '@backstage/backend-plugin-api';

import { newEnforcer, newModelFromString, StringAdapter } from 'casbin';
import {
Enforcer,
newEnforcer,
newModelFromString,
StringAdapter,
} from 'casbin';

import { AuditLogger } from '@janus-idp/backstage-plugin-audit-log-node';
import {
RBACProvider,
RBACProviderConnection,
} from '@janus-idp/backstage-plugin-rbac-node';

import {
HANDLE_RBAC_DATA_STAGE,
PermissionAuditInfo,
PermissionEvents,
RBAC_BACKEND,
RoleAuditInfo,
RoleEvents,
} from '../audit-log/audit-logger';
import { RoleMetadataStorage } from '../database/role-metadata';
import {
groupPoliciesToString,
Expand All @@ -27,22 +41,20 @@ class Connection implements RBACProviderConnection {
private readonly enforcer: EnforcerDelegate,
private readonly roleMetadataStorage: RoleMetadataStorage,
private readonly logger: LoggerService,
private readonly auditLogger: AuditLogger,
) {}

async applyRoles(roles: string[][]): Promise<void> {
const providerRoles: string[][] = [];

const stringPolicy = groupPoliciesToString(roles);

const providerRoles: string[][] = [];

const tempEnforcer = await newEnforcer(
newModelFromString(MODEL),
new StringAdapter(stringPolicy),
);

const currentRoles = await this.roleMetadataStorage.filterRoleMetadata(
this.id,
);
const providerRoleMetadata = currentRoles.map(meta => meta.roleEntityRef);
const providerRoleMetadata = await this.getProviderRoleMetadata();

// Get the roles for this provider coming from rbac plugin
for (const providerRole of providerRoleMetadata) {
Expand All @@ -53,33 +65,38 @@ class Connection implements RBACProviderConnection {

// Remove role
// role exists in rbac but does not exist in provider
for (const role of providerRoles) {
if (!(await tempEnforcer.hasGroupingPolicy(...role))) {
const roleMeta = await this.roleMetadataStorage.findRoleMetadata(
role[1],
);
await this.removeRoles(providerRoles, tempEnforcer);

const currentRole = await this.enforcer.getFilteredGroupingPolicy(
1,
role[1],
);
// Add the role
// role exists in provider but does not exist in rbac
await this.addRoles(roles);
}

if (!roleMeta) {
throw new Error('role does not exist');
}
async applyPermissions(permissions: string[][]): Promise<void> {
const stringPolicy = permissionPoliciesToString(permissions);

// Only one role exists in rbac remove role metadata as well
if (roleMeta && currentRole.length === 1) {
await this.enforcer.removeGroupingPolicy(role, roleMeta);
continue; // Move on to the next role
}
const providerPermissions: string[][] = [];

await this.enforcer.removeGroupingPolicy(role, roleMeta, true);
}
const tempEnforcer = await newEnforcer(
newModelFromString(MODEL),
new StringAdapter(stringPolicy),
);

const providerRoleMetadata = await this.getProviderRoleMetadata();

// Get the roles for this provider coming from rbac plugin
for (const providerRole of providerRoleMetadata) {
providerPermissions.push(
...(await this.enforcer.getFilteredPolicy(0, providerRole)),
);
}

// Add the role
// role exists in provider but does not exist in rbac
await this.removePermissions(providerPermissions, tempEnforcer);

await this.addPermissions(permissions);
}

private async addRoles(roles: string[][]): Promise<void> {
for (const role of roles) {
if (!(await this.enforcer.hasGroupingPolicy(...role))) {
const err = await validateGroupingPolicy(
Expand All @@ -93,54 +110,97 @@ class Connection implements RBACProviderConnection {
continue; // Skip adding this role as there was an error
}

const roleMeta = await this.roleMetadataStorage.findRoleMetadata(
role[1],
);
let roleMeta = await this.roleMetadataStorage.findRoleMetadata(role[1]);

const eventName = roleMeta
? RoleEvents.UPDATE_ROLE
: RoleEvents.CREATE_ROLE;
const message = roleMeta ? 'Updated role' : 'Created role';

// role does not exist in rbac, create the metadata for it
if (!roleMeta) {
const roleMetadata = {
roleMeta = {
modifiedBy: this.id,
source: this.id,
roleEntityRef: role[1],
};
await this.enforcer.addGroupingPolicy(role, roleMetadata); // <- TODO more knex trax issues
continue; // Move on to the next role
}

await this.enforcer.addGroupingPolicy(role, roleMeta);

await this.auditLogger.auditLog<RoleAuditInfo>({
actorId: RBAC_BACKEND,
message,
eventName,
metadata: { ...roleMeta, members: [role[0]] },
stage: HANDLE_RBAC_DATA_STAGE,
status: 'succeeded',
});
}
}
}

async applyPermissions(permissions: string[][]): Promise<void> {
const stringPolicy = permissionPoliciesToString(permissions);
private async removeRoles(
providerRoles: string[][],
tempEnforcer: Enforcer,
): Promise<void> {
// Remove role
// role exists in rbac but does not exist in provider
for (const role of providerRoles) {
if (!(await tempEnforcer.hasGroupingPolicy(...role))) {
const roleMeta = await this.roleMetadataStorage.findRoleMetadata(
role[1],
);

const providerPermissions: string[][] = [];
const currentRole = await this.enforcer.getFilteredGroupingPolicy(
1,
role[1],
);

const tempEnforcer = await newEnforcer(
newModelFromString(MODEL),
new StringAdapter(stringPolicy),
);
if (!roleMeta) {
throw new Error('role does not exist');
}

const currentRoles = await this.roleMetadataStorage.filterRoleMetadata(
this.id,
);
const providerRoleMetadata = currentRoles.map(meta => meta.roleEntityRef);
const eventName =
roleMeta && currentRole.length === 1
? RoleEvents.DELETE_ROLE
: RoleEvents.UPDATE_ROLE;
const message =
roleMeta && currentRole.length === 1
? 'Deleted role'
: 'Updated role: deleted members';

// Get the roles for this provider coming from rbac plugin
for (const providerRole of providerRoleMetadata) {
providerPermissions.push(
...(await this.enforcer.getFilteredPolicy(0, providerRole)),
);
}
// Only one role exists in rbac remove role metadata as well
if (roleMeta && currentRole.length === 1) {
await this.enforcer.removeGroupingPolicy(role, roleMeta);

for (const permission of providerPermissions) {
if (!(await tempEnforcer.hasPolicy(...permission))) {
this.enforcer.removePolicy(permission);
await this.auditLogger.auditLog<RoleAuditInfo>({
actorId: RBAC_BACKEND,
message,
eventName,
metadata: { ...roleMeta, members: [role[0]] },
stage: HANDLE_RBAC_DATA_STAGE,
status: 'succeeded',
});

continue; // Move on to the next role
}

await this.enforcer.removeGroupingPolicy(role, roleMeta, true);

await this.auditLogger.auditLog<RoleAuditInfo>({
actorId: RBAC_BACKEND,
message,
eventName,
metadata: { ...roleMeta, members: [role[0]] },
stage: HANDLE_RBAC_DATA_STAGE,
status: 'succeeded',
});
}
}
}

private async addPermissions(permissions: string[][]): Promise<void> {
for (const permission of permissions) {
if (!(await this.enforcer.hasPolicy(...permission))) {
const transformedPolicy = transformArrayToPolicy(permission);
Expand All @@ -163,10 +223,53 @@ class Connection implements RBACProviderConnection {
}

await this.enforcer.addPolicy(permission);

await this.auditLogger.auditLog<PermissionAuditInfo>({
actorId: RBAC_BACKEND,
message: `Created policy`,
eventName: PermissionEvents.CREATE_POLICY,
metadata: { policies: [permission], source: this.id },
stage: HANDLE_RBAC_DATA_STAGE,
status: 'succeeded',
});
}
}
}

private async removePermissions(
providerPermissions: string[][],
tempEnforcer: Enforcer,
): Promise<void> {
const removedPermissions: string[][] = [];
for (const permission of providerPermissions) {
if (!(await tempEnforcer.hasPolicy(...permission))) {
this.enforcer.removePolicy(permission);
removedPermissions.push(permission);
}

if (removedPermissions.length > 0) {
await this.auditLogger.auditLog<PermissionAuditInfo>({
actorId: RBAC_BACKEND,
message: `Deleted policies`,
eventName: PermissionEvents.DELETE_POLICY,
metadata: {
policies: removedPermissions,
source: this.id,
},
stage: HANDLE_RBAC_DATA_STAGE,
status: 'succeeded',
});
}
}
}

private async getProviderRoleMetadata(): Promise<string[]> {
const currentRoles = await this.roleMetadataStorage.filterRoleMetadata(
this.id,
);
return currentRoles.map(meta => meta.roleEntityRef);
}

async refresh(): Promise<void> {
console.log('refresh for manually refreshing');
}
Expand All @@ -177,6 +280,7 @@ export async function connectRBACProviders(
enforcer: EnforcerDelegate,
roleMetadataStorage: RoleMetadataStorage,
logger: LoggerService,
auditLogger: AuditLogger,
) {
await Promise.all(
providers.map(async provider => {
Expand All @@ -185,6 +289,7 @@ export async function connectRBACProviders(
enforcer,
roleMetadataStorage,
logger,
auditLogger,
);
return provider.connect(connection);
}),
Expand Down
17 changes: 10 additions & 7 deletions plugins/rbac-backend/src/service/policy-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class PolicyBuilder {
userInfo: UserInfoService;
},
pluginIdProvider: PluginIdProvider = { getPluginIds: () => [] },
rbacProviders: Array<RBACProvider>,
rbacProviders?: Array<RBACProvider>,
): Promise<Router> {
const isPluginEnabled = env.config.getOptionalBoolean('permission.enabled');
if (isPluginEnabled) {
Expand Down Expand Up @@ -111,12 +111,15 @@ export class PolicyBuilder {
httpAuthService: httpAuth,
});

await connectRBACProviders(
rbacProviders,
enforcerDelegate,
roleMetadataStorage,
env.logger,
);
if (rbacProviders) {
await connectRBACProviders(
rbacProviders,
enforcerDelegate,
roleMetadataStorage,
env.logger,
defAuditLog,
);
}

const pluginIdsConfig = env.config.getOptionalStringArray(
'permission.rbac.pluginsWithPermission',
Expand Down
4 changes: 2 additions & 2 deletions plugins/rbac-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
} from '@backstage/plugin-permission-common';

export type Source =
| string
| 'rest' // created via REST API
| 'csv-file' // created via policies-csv-file with defined path in the application configuration
| 'configuration' // created from application configuration
| 'legacy' // preexisting policies
| string;
| 'legacy'; // preexisting policies

export type PermissionPolicyMetadata = {
source: Source;
Expand Down

0 comments on commit ef84e2c

Please sign in to comment.