diff --git a/.vscode/bookmarks.json b/.vscode/bookmarks.json
new file mode 100644
index 0000000..cf0f66a
--- /dev/null
+++ b/.vscode/bookmarks.json
@@ -0,0 +1,44 @@
+{
+ "files": [
+ {
+ "path": "node_modules/@backstage/plugin-catalog-backend/dist/cjs/CatalogBuilder-CGSl8LEN.cjs.js",
+ "bookmarks": [
+ {
+ "line": 2369,
+ "column": 0,
+ "label": "FilterQueryBuilder"
+ },
+ {
+ "line": 2452,
+ "column": 0,
+ "label": "DB Query execution"
+ },
+ {
+ "line": 5559,
+ "column": 0,
+ "label": "Catalog entities router "
+ },
+ {
+ "line": 7111,
+ "column": 0,
+ "label": "Catalog permission router"
+ },
+ {
+ "line": 7170,
+ "column": 0,
+ "label": "CatalogRoute creation"
+ }
+ ]
+ },
+ {
+ "path": "node_modules/@backstage/plugin-permission-node/dist/index.cjs.js",
+ "bookmarks": [
+ {
+ "line": 26,
+ "column": 0,
+ "label": "Permission JSON creation fn"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 2916455..b431f68 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -382,7 +382,11 @@ const routes = (
*/}
}
+ element={
+
+
+
+ }
>
{entityPage}
diff --git a/packages/backend/src/extensions/catalogPermissionRules.ts b/packages/backend/src/extensions/catalogPermissionRules.ts
new file mode 100644
index 0000000..2237e34
--- /dev/null
+++ b/packages/backend/src/extensions/catalogPermissionRules.ts
@@ -0,0 +1,16 @@
+import { createBackendModule } from '@backstage/backend-plugin-api';
+import { catalogPermissionExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
+import { isHaveRepositoryAccess } from '../premissions/repository.rule';
+
+export default createBackendModule({
+ pluginId: 'catalog',
+ moduleId: 'permission-rules',
+ register(reg) {
+ reg.registerInit({
+ deps: { catalog: catalogPermissionExtensionPoint },
+ async init({ catalog }) {
+ catalog.addPermissionRules(isHaveRepositoryAccess);
+ },
+ });
+ },
+});
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index f9cce33..8913237 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -37,6 +37,7 @@ backend.add(import('@backstage/plugin-catalog-backend/alpha'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
+backend.add(import('./extensions/catalogPermissionRules'));
// See https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors
backend.add(import('@backstage/plugin-catalog-backend-module-logs'));
diff --git a/packages/backend/src/plugins/permission.ts b/packages/backend/src/plugins/permission.ts
index 6a77d6c..7755374 100644
--- a/packages/backend/src/plugins/permission.ts
+++ b/packages/backend/src/plugins/permission.ts
@@ -3,14 +3,11 @@ import {
isPermission,
type PolicyDecision,
} from '@backstage/plugin-permission-common';
-import type {
- PermissionPolicy,
- PolicyQuery,
-} from '@backstage/plugin-permission-node';
import {
- catalogConditions,
- createCatalogConditionalDecision,
-} from '@backstage/plugin-catalog-backend/alpha';
+ type PermissionPolicy,
+ type PolicyQuery,
+} from '@backstage/plugin-permission-node';
+import { createCatalogConditionalDecision } from '@backstage/plugin-catalog-backend/alpha';
import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common/alpha';
import type { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import type { PaginatingEndpoints } from '@octokit/plugin-paginate-rest';
@@ -26,6 +23,7 @@ import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
import { coreServices } from '@backstage/backend-plugin-api';
import { Octokit } from 'octokit';
import * as _ from 'lodash';
+import { RepositoryAccessCondition as repositoryAccessCondition } from '../premissions/repository.rule';
class RequestPermissionPolicy implements PermissionPolicy {
readonly orgRepositories: Promise<
@@ -73,7 +71,7 @@ class RequestPermissionPolicy implements PermissionPolicy {
}
this.logger.info("didn't received any handler for the policy request", {
- request,
+ request: JSON.stringify(request, null, 2),
});
return { result: AuthorizeResult.ALLOW };
}
@@ -89,9 +87,9 @@ class RequestPermissionPolicy implements PermissionPolicy {
org: String(process.env.GITHUB_ORGANIZATION),
},
)) {
- //! will use status or header to validate success
out.push(...data);
}
+ this.logger.info('***GithubRequest GET /orgs/{org}/repos', {});
this.logger.debug('Github Repo List resolution benchmark', {
totalTimeInMilliSeconds: startTimeBenchmark - performance.now(),
});
@@ -113,6 +111,10 @@ class RequestPermissionPolicy implements PermissionPolicy {
Authorization: `Bearer ${token}`,
},
});
+ this.logger.info(
+ '***CatalogRequest GET /entities?filter=kind=component',
+ {},
+ );
const data = await req.json();
// Get Name a repo name not the repo full_name
return new Set(
@@ -120,6 +122,40 @@ class RequestPermissionPolicy implements PermissionPolicy {
);
}
+ protected async fetchUserRole(userEntityRef: string) {
+ const { name: username } = parseEntityRef(userEntityRef);
+
+ const response = await this.octokit
+ .request('GET /orgs/{org}/teams/{team_slug}/memberships/{username}', {
+ org: String(process.env.GITHUB_ORGANIZATION),
+ team_slug: String(process.env.REPO_CREATOR_TEAM),
+ username,
+ })
+ .catch(e => {
+ this.logger.debug('Issue while fetching details for the username', {
+ error: JSON.stringify(e),
+ });
+ });
+
+ this.logger.info(
+ '***GithubRequest GET /orgs/{org}/teams/{team_slug}/memberships/{username}',
+ {
+ org: String(process.env.GITHUB_ORGANIZATION),
+ team_slug: String(process.env.REPO_CREATOR_TEAM),
+ },
+ );
+ const ok = 200;
+ if (
+ response &&
+ response.status === ok &&
+ response.data.state === 'active'
+ ) {
+ return response.data.role ?? 'null';
+ }
+
+ return 'null';
+ }
+
protected async resolveAuthorizedRepoList(
userEntityRef: string,
): Promise {
@@ -141,24 +177,42 @@ class RequestPermissionPolicy implements PermissionPolicy {
this.userRepoPermissions[userEntityRef].push(repo.name);
}
- for (const repos of _.chunk(privateCatalogRepos, 10)) {
- const permissions = await Promise.all(
- repos.map(repo =>
- this.octokit.rest.repos
- .getCollaboratorPermissionLevel({
- owner: String(process.env.GITHUB_ORGANIZATION),
- repo: repo.name,
- username: usernameEntity.name,
- })
- .then(resp => ({
- repo,
- ...resp.data,
- })),
- ),
- );
+ const userRole = await this.fetchUserRole(userEntityRef);
- for (const permission of permissions) {
- this.userRepoPermissions[userEntityRef].push(permission.repo.name);
+ if (['member', 'admin', 'maintainer'].includes(userRole)) {
+ for (const repos of privateCatalogRepos) {
+ this.userRepoPermissions[userEntityRef].push(repos.name);
+ }
+ } else {
+ // will fetch individual repo permissions
+ for (const repos of _.chunk(privateCatalogRepos, 10)) {
+ const permissions = await Promise.all(
+ repos.map(repo =>
+ this.octokit.rest.repos
+ .getCollaboratorPermissionLevel({
+ owner: String(process.env.GITHUB_ORGANIZATION),
+ repo: repo.name,
+ username: usernameEntity.name,
+ })
+ .then(resp => ({
+ repo,
+ ...resp.data,
+ }))
+ .finally(() => {
+ this.logger.info(
+ '***GithubRequest GET /repos/{owner}/{repo}/collaborators/{username}/permission',
+ {
+ owner: String(process.env.GITHUB_ORGANIZATION),
+ repo: repo.name,
+ },
+ );
+ }),
+ ),
+ );
+
+ for (const permission of permissions) {
+ this.userRepoPermissions[userEntityRef].push(permission.repo.name);
+ }
}
}
@@ -179,8 +233,15 @@ class RequestPermissionPolicy implements PermissionPolicy {
user.identity.userEntityRef,
);
- if (!userPermission) {
+ if (!userPermission?.length) {
// permission not resolved from the Github API
+ this.logger.info(
+ "Not able to fetch user Permission or Github didn't have repos for the user",
+ {
+ user: user.identity.userEntityRef,
+ resolvedPermission: JSON.stringify(userPermission),
+ },
+ );
return { result: AuthorizeResult.DENY };
}
this.logger.debug('Permission resolution benchmark', {
@@ -189,13 +250,11 @@ class RequestPermissionPolicy implements PermissionPolicy {
this.logger.debug('Permission resolution benchmark', {
totalTimeInMilliSeconds: startTimeBenchmark - performance.now(),
});
- return createCatalogConditionalDecision(request.permission, {
- //@ts-ignore
- anyOf: userPermission.map(value =>
- //@ts-ignore
- catalogConditions.hasMetadata({ key: 'name', value }),
- ),
- });
+ const condition = createCatalogConditionalDecision(
+ request.permission,
+ repositoryAccessCondition({ repos: userPermission }),
+ );
+ return condition;
}
}
@@ -214,6 +273,8 @@ export default createBackendModule({
async init({ policy, cache, discovery, auth, logger }) {
const octokit = new Octokit({
auth: String(process.env.GITHUB_TOKEN),
+ // in case of custom option for throttle follow below
+ // https://github.com/octokit/plugin-throttling.js/#readme
// throttle: {},
});
policy.setPolicy(
diff --git a/packages/backend/src/premissions/repository.rule.ts b/packages/backend/src/premissions/repository.rule.ts
new file mode 100644
index 0000000..03f38c9
--- /dev/null
+++ b/packages/backend/src/premissions/repository.rule.ts
@@ -0,0 +1,42 @@
+import type { Entity } from '@backstage/catalog-model';
+import { z } from 'zod';
+import { createCatalogPermissionRule } from '@backstage/plugin-catalog-backend/alpha';
+import { createConditionFactory } from '@backstage/plugin-permission-node';
+
+export const isHaveRepositoryAccess = createCatalogPermissionRule({
+ name: 'IS_HAVE_REPO_ACCESS',
+ description: 'Checks if entity have repository access',
+ resourceType: 'catalog-entity',
+ paramsSchema: z.object({
+ repos: z.string().array().describe('name of repositories to check'),
+ }),
+ apply: (resource: Entity, { repos }) => {
+ if (!resource.relations) {
+ return false;
+ }
+
+ return resource.relations
+ .filter(relation => relation.type === 'partOf')
+ .some(relation => repos.includes(relation.targetRef));
+ },
+ toQuery: ({ repos }) => {
+ // it will add sql query to check repo or all the templates
+ // "metadata.name" in ["repoName"] or kind in ['template']
+ return {
+ anyOf: [
+ {
+ key: 'metadata.name',
+ values: repos,
+ },
+ {
+ // Skip from the check it will allow all the templates
+ key: 'kind',
+ values: ['template'],
+ },
+ ],
+ };
+ },
+});
+export const RepositoryAccessCondition = createConditionFactory(
+ isHaveRepositoryAccess,
+);
diff --git a/plugins/jenkins-with-reporting-backend-backend/src/service/router.ts b/plugins/jenkins-with-reporting-backend-backend/src/service/router.ts
index 26dacc2..ff1acb5 100644
--- a/plugins/jenkins-with-reporting-backend-backend/src/service/router.ts
+++ b/plugins/jenkins-with-reporting-backend-backend/src/service/router.ts
@@ -155,6 +155,7 @@ export async function createRouter(
},
);
+
const middleware = MiddlewareFactory.create({ logger, config });
router.use(middleware.error());
diff --git a/plugins/jenkins-with-reporting/src/api/JenkinsApi.ts b/plugins/jenkins-with-reporting/src/api/JenkinsApi.ts
index 0118391..2ec66ef 100644
--- a/plugins/jenkins-with-reporting/src/api/JenkinsApi.ts
+++ b/plugins/jenkins-with-reporting/src/api/JenkinsApi.ts
@@ -164,6 +164,7 @@ export class JenkinsClient implements JenkinsApi {
const url = new URL(
`${await this.discoveryApi.getBaseUrl(
'jenkins-with-reporting-backend',
+
)}/v1/entity/${encodeURIComponent(entity.namespace)}/${encodeURIComponent(
entity.kind,
)}/${encodeURIComponent(entity.name)}/projects`,