From 3f9cad3876747c34defb9cb21848a9973e722003 Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Fri, 25 Oct 2024 18:54:32 +0300 Subject: [PATCH 1/2] fix(rbac): fix more sonar cloud issue for rbac-backend Signed-off-by: Oleksandr Andriienko --- .../src/conditional-aliases/alias-resolver.ts | 4 +- .../src/database/casbin-adapter-factory.ts | 22 +-- .../src/database/role-metadata.ts | 2 +- .../file-permissions/csv-file-watcher.test.ts | 2 +- .../src/file-permissions/csv-file-watcher.ts | 93 ++++++++----- .../yaml-conditional-file-watcher.ts | 2 +- .../src/policies/permission-policy.ts | 12 +- .../src/role-manager/ancestor-search-memo.ts | 12 +- .../src/role-manager/role-manager.test.ts | 9 +- .../src/role-manager/role-manager.ts | 8 +- .../src/service/plugin-endpoint.test.ts | 130 +++++++++--------- .../src/service/plugin-endpoints.ts | 12 +- 12 files changed, 172 insertions(+), 136 deletions(-) diff --git a/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts b/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts index 611225e45f..ae1b0faa43 100644 --- a/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts +++ b/plugins/rbac-backend/src/conditional-aliases/alias-resolver.ts @@ -12,9 +12,7 @@ import { ConditionalAliases, } from '@janus-idp/backstage-plugin-rbac-common'; -interface Predicate { - (item: T): boolean; -} +type Predicate = (item: T) => boolean; function isOwnerRefsAlias(value: PermissionRuleParam): boolean { const alias = `${CONDITION_ALIAS_SIGN}${ConditionalAliases.OWNER_REFS}`; diff --git a/plugins/rbac-backend/src/database/casbin-adapter-factory.ts b/plugins/rbac-backend/src/database/casbin-adapter-factory.ts index b4bd5389b4..6b4507abd6 100644 --- a/plugins/rbac-backend/src/database/casbin-adapter-factory.ts +++ b/plugins/rbac-backend/src/database/casbin-adapter-factory.ts @@ -22,32 +22,32 @@ export class CasbinDBAdapterFactory { const client = databaseConfig?.getOptionalString('client'); let adapter; - if (client === 'pg') { + if (databaseConfig && client === 'pg') { const dbName = await this.databaseClient.client.config.connection.database; const schema = (await this.databaseClient.client.searchPath?.[0]) ?? 'public'; - const ssl = this.handlePostgresSSL(databaseConfig!); + const ssl = this.handlePostgresSSL(databaseConfig); adapter = await TypeORMAdapter.newAdapter({ type: 'postgres', - host: databaseConfig?.getString('connection.host'), - port: databaseConfig?.getNumber('connection.port'), - username: databaseConfig?.getString('connection.user'), - password: databaseConfig?.getString('connection.password'), + host: databaseConfig.getString('connection.host'), + port: databaseConfig.getNumber('connection.port'), + username: databaseConfig.getString('connection.user'), + password: databaseConfig.getString('connection.password'), ssl, database: dbName, schema: schema, }); } - if (client === 'better-sqlite3') { + if (databaseConfig && client === 'better-sqlite3') { let storage; - if (typeof databaseConfig?.get('connection')?.valueOf() === 'string') { - storage = databaseConfig?.getString('connection'); - } else if (databaseConfig?.has('connection.directory')) { - const storageDir = databaseConfig?.getString('connection.directory'); + if (typeof databaseConfig.get('connection')?.valueOf() === 'string') { + storage = databaseConfig.getString('connection'); + } else if (databaseConfig.has('connection.directory')) { + const storageDir = databaseConfig.getString('connection.directory'); storage = resolve(storageDir, DEFAULT_SQLITE3_STORAGE_FILE_NAME); } diff --git a/plugins/rbac-backend/src/database/role-metadata.ts b/plugins/rbac-backend/src/database/role-metadata.ts index e80f463d8a..c774d5dc39 100644 --- a/plugins/rbac-backend/src/database/role-metadata.ts +++ b/plugins/rbac-backend/src/database/role-metadata.ts @@ -143,7 +143,7 @@ export class DataBaseRoleMetadataStorage implements RoleMetadataStorage { await trx(ROLE_METADATA_TABLE) .delete() - .whereIn('id', [metadataDao.id!]); + .whereIn('id', [metadataDao.id]); } } diff --git a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts index 79afe666d3..bddc6771c2 100644 --- a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts +++ b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts @@ -224,7 +224,7 @@ describe('CSVFileWatcher', () => { await enforcerDelegate.addPolicy(legacyPermission); await enforcerDelegate.addGroupingPolicies( [['user:default/guest', 'role:default/legacy']], - legacyRoleMetadata!, + legacyRoleMetadata, ); roleMetadataStorageMock.filterRoleMetadata = jest .fn() diff --git a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts index 5c1070a65e..4f633ca406 100644 --- a/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts +++ b/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts @@ -46,7 +46,8 @@ type CSVFilePolicies = { export class CSVFileWatcher extends AbstractFileWatcher { private currentContent: string[][]; - private csvFilePolicies: CSVFilePolicies; + + private readonly csvFilePolicies: CSVFilePolicies; constructor( filePath: string | undefined, @@ -94,6 +95,7 @@ export class CSVFileWatcher extends AbstractFileWatcher { if (!this.filePath) { return; } + let content: string[][] = []; // If the file is set load the file contents content = this.parse(); @@ -103,33 +105,59 @@ export class CSVFileWatcher extends AbstractFileWatcher { new FileAdapter(this.filePath), ); - // Check for any old policies that will need to be removed by checking if - // the policy no longer exists in the temp enforcer (csv file) + await this.processOldPolicies(tempEnforcer); + await this.processNewPolicies(tempEnforcer); + + await this.migrateLegacyMetadata(tempEnforcer); + + // We pass current here because this is during initialization and it has not changed yet + await this.updatePolicies(content, tempEnforcer); + + if (this.allowReload) { + this.watchFile(); + } + } + + private async processOldPolicies(tempEnforcer: Enforcer): Promise { const roleMetadatas = await this.roleMetadataStorage.filterRoleMetadata('csv-file'); const fileRoles = roleMetadatas.map(meta => meta.roleEntityRef); if (fileRoles.length > 0) { - const groupingPoliciesToRemove = - await this.enforcer.getFilteredGroupingPolicy(1, ...fileRoles); - for (const gPolicy of groupingPoliciesToRemove) { - if (!(await tempEnforcer.hasGroupingPolicy(...gPolicy))) { - this.csvFilePolicies.removedGroupPolicies.push(gPolicy); - } + await this.checkPoliciesToRemove(fileRoles, tempEnforcer); + await this.checkGroupingPoliciesToRemove(fileRoles, tempEnforcer); + } + } + + private async checkPoliciesToRemove( + fileRoles: string[], + tempEnforcer: Enforcer, + ): Promise { + const policiesToRemove = await this.enforcer.getFilteredPolicy( + 0, + ...fileRoles, + ); + for (const policy of policiesToRemove) { + if (!(await tempEnforcer.hasPolicy(...policy))) { + this.csvFilePolicies.removedPolicies.push(policy); } - const policiesToRemove = await this.enforcer.getFilteredPolicy( - 0, - ...fileRoles, - ); - for (const policy of policiesToRemove) { - if (!(await tempEnforcer.hasPolicy(...policy))) { - this.csvFilePolicies.removedPolicies.push(policy); - } + } + } + + private async checkGroupingPoliciesToRemove( + fileRoles: string[], + tempEnforcer: Enforcer, + ): Promise { + const groupingPoliciesToRemove = + await this.enforcer.getFilteredGroupingPolicy(1, ...fileRoles); + for (const gPolicy of groupingPoliciesToRemove) { + if (!(await tempEnforcer.hasGroupingPolicy(...gPolicy))) { + this.csvFilePolicies.removedGroupPolicies.push(gPolicy); } } + } - // Check for any new policies that need to be added by checking if - // the policy does not currently exist in the enforcer + private async processNewPolicies(tempEnforcer: Enforcer): Promise { const policiesToAdd = await tempEnforcer.getPolicy(); const groupPoliciesToAdd = await tempEnforcer.getGroupingPolicy(); @@ -144,15 +172,6 @@ export class CSVFileWatcher extends AbstractFileWatcher { this.csvFilePolicies.addedGroupPolicies.push(groupPolicy); } } - - await this.migrateLegacyMetadata(tempEnforcer); - - // We pass current here because this is during initialization and it has not changed yet - await this.updatePolicies(content, tempEnforcer); - - if (this.allowReload) { - this.watchFile(); - } } // Check for policies that might need to be updated @@ -201,11 +220,15 @@ export class CSVFileWatcher extends AbstractFileWatcher { * It will finally call updatePolicies with the new content. */ async onChange(): Promise { + if (!this.filePath) { + throw new Error('File path is not specified'); + } + const newContent = this.parse(); const tempEnforcer = await newEnforcer( newModelFromString(MODEL), - new FileAdapter(this.filePath!), + new FileAdapter(this.filePath), ); const currentFlatContent = this.currentContent.flatMap(data => { @@ -280,6 +303,10 @@ export class CSVFileWatcher extends AbstractFileWatcher { * @param tempEnforcer Temporary enforcer for checking for duplicates when adding policies */ private async addPermissionPolicies(tempEnforcer: Enforcer): Promise { + if (!this.filePath) { + throw new Error('File path is not specified'); + } + for (const policy of this.csvFilePolicies.addedPolicies) { const transformedPolicy = transformArrayToPolicy(policy); const metadata = await this.roleMetadataStorage.findRoleMetadata( @@ -305,7 +332,7 @@ export class CSVFileWatcher extends AbstractFileWatcher { err = await checkForDuplicatePolicies( tempEnforcer, policy, - this.filePath!, + this.filePath, ); if (err) { this.logger.warn(err.message); @@ -367,6 +394,10 @@ export class CSVFileWatcher extends AbstractFileWatcher { * @param tempEnforcer Temporary enforcer for checking for duplicates when adding policies */ private async addRoles(tempEnforcer: Enforcer): Promise { + if (!this.filePath) { + throw new Error('File path is not specified'); + } + for (const groupPolicy of this.csvFilePolicies.addedGroupPolicies) { let err = await validateGroupingPolicy( groupPolicy, @@ -383,7 +414,7 @@ export class CSVFileWatcher extends AbstractFileWatcher { err = await checkForDuplicateGroupPolicies( tempEnforcer, groupPolicy, - this.filePath!, + this.filePath, ); if (err) { this.logger.warn(err.message); diff --git a/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts b/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts index 40a810a4d3..ec5957eb74 100644 --- a/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts +++ b/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts @@ -212,7 +212,7 @@ export class YamlConditinalPoliciesFileWatcher extends AbstractFileWatcher< condition.permissionMapping, ) )[0]; - await this.conditionalStorage.deleteCondition(conditionToDelete.id!); + await this.conditionalStorage.deleteCondition(conditionToDelete.id); await this.auditLogger.auditLog({ message: `Deleted conditional permission policy`, diff --git a/plugins/rbac-backend/src/policies/permission-policy.ts b/plugins/rbac-backend/src/policies/permission-policy.ts index 7bdf5b67fb..93b0f3d1c2 100644 --- a/plugins/rbac-backend/src/policies/permission-policy.ts +++ b/plugins/rbac-backend/src/policies/permission-policy.ts @@ -58,8 +58,6 @@ const evaluatePermMsg = ( } and action '${toPermissionAction(permission.attributes)}'`; export class RBACPermissionPolicy implements PermissionPolicy { - private readonly superUserList?: string[]; - public static async build( logger: LoggerService, auditLogger: AuditLogger, @@ -162,10 +160,8 @@ export class RBACPermissionPolicy implements PermissionPolicy { private readonly enforcer: EnforcerDelegate, private readonly auditLogger: AuditLogger, private readonly conditionStorage: ConditionalStorage, - superUserList?: string[], - ) { - this.superUserList = superUserList; - } + private readonly superUsers: string[], + ) {} async handle( request: PolicyQuery, @@ -278,13 +274,13 @@ export class RBACPermissionPolicy implements PermissionPolicy { return false; } - private isAuthorized = async ( + private readonly isAuthorized = async ( userIdentity: string, permission: string, action: string, roles: string[], ): Promise => { - if (this.superUserList!.includes(userIdentity)) { + if (this.superUsers.includes(userIdentity)) { return true; } diff --git a/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts index f3bafff23b..cdfc9dea35 100644 --- a/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts +++ b/plugins/rbac-backend/src/role-manager/ancestor-search-memo.ts @@ -17,14 +17,14 @@ export type ASMGroup = Relation | Entity; // Also AncestorSearchMemo supports detection cycle dependencies between groups in the graph. // export class AncestorSearchMemo { - private graph: Graph; + private readonly graph: Graph; - private catalogApi: CatalogApi; - private catalogDBClient: Knex; - private auth: AuthService; + private readonly catalogApi: CatalogApi; + private readonly catalogDBClient: Knex; + private readonly auth: AuthService; - private userEntityRef: string; - private maxDepth?: number; + private readonly userEntityRef: string; + private readonly maxDepth?: number; constructor( userEntityRef: string, diff --git a/plugins/rbac-backend/src/role-manager/role-manager.test.ts b/plugins/rbac-backend/src/role-manager/role-manager.test.ts index d4822a3cfb..fbce0935a0 100644 --- a/plugins/rbac-backend/src/role-manager/role-manager.test.ts +++ b/plugins/rbac-backend/src/role-manager/role-manager.test.ts @@ -73,9 +73,12 @@ describe('BackstageRoleManager', () => { describe('unimplemented methods', () => { it('should throw an error for syncedHasLink', () => { - expect(() => - roleManager.syncedHasLink!('user:default/role1', 'user:default/role2'), - ).toThrow('Method "syncedHasLink" not implemented.'); + expect(() => { + if (!roleManager.syncedHasLink) { + throw new Error('Method "syncedHasLink" is undefined.'); + } + roleManager.syncedHasLink('user:default/role1', 'user:default/role2'); + }).toThrow('Method "syncedHasLink" not implemented.'); }); it('should throw an error for getUsers', async () => { diff --git a/plugins/rbac-backend/src/role-manager/role-manager.ts b/plugins/rbac-backend/src/role-manager/role-manager.ts index 484c7eadb8..6490f8f4a7 100644 --- a/plugins/rbac-backend/src/role-manager/role-manager.ts +++ b/plugins/rbac-backend/src/role-manager/role-manager.ts @@ -10,8 +10,8 @@ import { AncestorSearchMemo } from './ancestor-search-memo'; import { RoleMemberList } from './member-list'; export class BackstageRoleManager implements RoleManager { - private allRoles: Map; - private maxDepth?: number; + private readonly allRoles: Map; + private readonly maxDepth?: number; constructor( private readonly catalogApi: CatalogApi, private readonly logger: LoggerService, @@ -23,7 +23,7 @@ export class BackstageRoleManager implements RoleManager { this.allRoles = new Map(); const rbacConfig = this.config.getOptionalConfig('permission.rbac'); this.maxDepth = rbacConfig?.getOptionalNumber('maxDepth'); - if (this.maxDepth !== undefined && this.maxDepth! < 0) { + if (this.maxDepth !== undefined && this.maxDepth < 0) { throw new Error( 'Max Depth for RBAC group hierarchy must be greater than or equal to zero', ); @@ -325,7 +325,7 @@ export class BackstageRoleManager implements RoleManager { this.allRoles.delete(name2); } - if (currentRole && currentRole.hasMember(name1)) { + if (currentRole?.hasMember(name1)) { return true; } diff --git a/plugins/rbac-backend/src/service/plugin-endpoint.test.ts b/plugins/rbac-backend/src/service/plugin-endpoint.test.ts index 86cf51b085..784c32dfed 100644 --- a/plugins/rbac-backend/src/service/plugin-endpoint.test.ts +++ b/plugins/rbac-backend/src/service/plugin-endpoint.test.ts @@ -10,6 +10,68 @@ const backendPluginIDsProviderMock = { }), }; +const mockUrlReaderServiceWithResourcePermission = mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","name":"policy.entity.read","attributes":{"action":"read"},"resourceType":"policy-entity"}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, +}); + +const mockUrlReaderServiceWithNonResourcedPermission = + mockServices.urlReader.mock({ + readUrl: async () => { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"basic","name":"catalog.entity.create","attributes":{"action":"create"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + }, + }); + +const mockUrlReaderServiceWithNotFoundPermission = mockServices.urlReader.mock({ + readUrl: async (wellKnownURL: string) => { + if ( + wellKnownURL === + 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + } + throw new NotFoundError(); + }, +}); + +const mockUrlReaderServiceWithErrorOnGetPermission = + mockServices.urlReader.mock({ + readUrl: async (wellKnownURL: string) => { + if ( + wellKnownURL === + 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' + ) { + return { + buffer: async () => { + return Buffer.from( + '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', + ); + }, + } as UrlReaderServiceReadUrlResponse; + } + throw new Error('Unexpected error'); + }, + }); + describe('plugin-endpoint', () => { const mockPluginEndpointDiscovery = mockServices.discovery.mock({ getBaseUrl: async (pluginId: string) => { @@ -37,18 +99,6 @@ describe('plugin-endpoint', () => { it('should return non empty plugin policies list with resourced permission', async () => { backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['permission']); - const mockUrlReaderService = mockServices.urlReader.mock({ - readUrl: async () => { - return { - buffer: async () => { - return Buffer.from( - '{"permissions":[{"type":"resource","name":"policy.entity.read","attributes":{"action":"read"},"resourceType":"policy-entity"}]}', - ); - }, - } as UrlReaderServiceReadUrlResponse; - }, - }); - const collector = new PluginPermissionMetadataCollector({ deps: { discovery: mockPluginEndpointDiscovery, @@ -56,7 +106,7 @@ describe('plugin-endpoint', () => { logger: mockServices.logger.mock(), config: mockServices.rootConfig(), }, - optional: { urlReader: mockUrlReaderService }, + optional: { urlReader: mockUrlReaderServiceWithResourcePermission }, }); const policiesMetadata = await collector.getPluginPolicies( mockServices.auth(), @@ -76,18 +126,6 @@ describe('plugin-endpoint', () => { it('should return non empty plugin policies list with non resourced permission', async () => { backendPluginIDsProviderMock.getPluginIds.mockReturnValue(['permission']); - const mockUrlReaderService = mockServices.urlReader.mock({ - readUrl: async () => { - return { - buffer: async () => { - return Buffer.from( - '{"permissions":[{"type":"basic","name":"catalog.entity.create","attributes":{"action":"create"}}]}', - ); - }, - } as UrlReaderServiceReadUrlResponse; - }, - }); - const collector = new PluginPermissionMetadataCollector({ deps: { discovery: mockPluginEndpointDiscovery, @@ -95,7 +133,7 @@ describe('plugin-endpoint', () => { logger: mockServices.logger.mock(), config: mockServices.rootConfig(), }, - optional: { urlReader: mockUrlReaderService }, + optional: { urlReader: mockUrlReaderServiceWithNonResourcedPermission }, }); const policiesMetadata = await collector.getPluginPolicies( mockServices.auth(), @@ -117,24 +155,6 @@ describe('plugin-endpoint', () => { 'unknown-plugin-id', ]); - const mockUrlReaderService = mockServices.urlReader.mock({ - readUrl: async (wellKnownURL: string) => { - if ( - wellKnownURL === - 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' - ) { - return { - buffer: async () => { - return Buffer.from( - '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', - ); - }, - } as UrlReaderServiceReadUrlResponse; - } - throw new NotFoundError(); - }, - }); - const logger = mockServices.logger.mock(); const errorSpy = jest.spyOn(logger, 'warn').mockClear(); const collector = new PluginPermissionMetadataCollector({ @@ -144,7 +164,7 @@ describe('plugin-endpoint', () => { logger, config: mockServices.rootConfig(), }, - optional: { urlReader: mockUrlReaderService }, + optional: { urlReader: mockUrlReaderServiceWithNotFoundPermission }, }); const policiesMetadata = await collector.getPluginPolicies( mockServices.auth(), @@ -171,24 +191,6 @@ describe('plugin-endpoint', () => { 'catalog', ]); - const mockUrlReaderService = mockServices.urlReader.mock({ - readUrl: async (wellKnownURL: string) => { - if ( - wellKnownURL === - 'https://localhost:7007/api/permission/.well-known/backstage/permissions/metadata' - ) { - return { - buffer: async () => { - return Buffer.from( - '{"permissions":[{"type":"resource","resourceType":"policy-entity","name":"policy.entity.read","attributes":{"action":"read"}}]}', - ); - }, - } as UrlReaderServiceReadUrlResponse; - } - throw new Error('Unexpected error'); - }, - }); - const logger = mockServices.logger.mock(); const errorSpy = jest.spyOn(logger, 'error').mockClear(); const collector = new PluginPermissionMetadataCollector({ @@ -198,7 +200,7 @@ describe('plugin-endpoint', () => { logger, config: mockServices.rootConfig(), }, - optional: { urlReader: mockUrlReaderService }, + optional: { urlReader: mockUrlReaderServiceWithErrorOnGetPermission }, }); const policiesMetadata = await collector.getPluginPolicies( diff --git a/plugins/rbac-backend/src/service/plugin-endpoints.ts b/plugins/rbac-backend/src/service/plugin-endpoints.ts index 591ba1cfa5..1611629dfe 100644 --- a/plugins/rbac-backend/src/service/plugin-endpoints.ts +++ b/plugins/rbac-backend/src/service/plugin-endpoints.ts @@ -90,18 +90,24 @@ export class PluginPermissionMetadataCollector { const pluginMetadata = await this.getPluginMetaData(auth); return pluginMetadata - .filter(metadata => metadata.metaDataResponse.permissions !== undefined) + .filter( + ( + metadata, + ): metadata is typeof metadata & { + metaDataResponse: { permissions: Permission[] }; + } => metadata.metaDataResponse.permissions !== undefined, + ) .map(metadata => { return { pluginId: metadata.pluginId, policies: permissionsToCasbinPolicies( - metadata.metaDataResponse.permissions!, + metadata.metaDataResponse.permissions, ), }; }); } - private static permissionFactory: ReaderFactory = () => { + private static readonly permissionFactory: ReaderFactory = () => { return [{ reader: new FetchUrlReader(), predicate: (_url: URL) => true }]; }; From a1f3e117e56fb4be89a33137eaad76c82d28778a Mon Sep 17 00:00:00 2001 From: Oleksandr Andriienko Date: Mon, 28 Oct 2024 12:44:24 +0200 Subject: [PATCH 2/2] fix(rbac): add changeset Signed-off-by: Oleksandr Andriienko --- .changeset/friendly-swans-return.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/friendly-swans-return.md diff --git a/.changeset/friendly-swans-return.md b/.changeset/friendly-swans-return.md new file mode 100644 index 0000000000..d5cae3a0f4 --- /dev/null +++ b/.changeset/friendly-swans-return.md @@ -0,0 +1,5 @@ +--- +"@janus-idp/backstage-plugin-rbac-backend": patch +--- + +Fix some sonar cloud issues.