Skip to content

Commit

Permalink
Merge branch 'main' into ARC-jenkins-plugin-1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sadarunnisa-sf authored Dec 24, 2024
2 parents 1fbfcd3 + 4f56c01 commit fb02501
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 35 deletions.
44 changes: 44 additions & 0 deletions .vscode/bookmarks.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
6 changes: 5 additions & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,11 @@ const routes = (
</Route> */}
<Route
path="/catalog/:namespace/:kind/:name"
element={<CatalogEntityPage />}
element={
<RequirePermission permission={catalogEntityCreatePermission}>
<CatalogEntityPage />
</RequirePermission>
}
>
{entityPage}
</Route>
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/extensions/catalogPermissionRules.ts
Original file line number Diff line number Diff line change
@@ -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);
},
});
},
});
1 change: 1 addition & 0 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
129 changes: 95 additions & 34 deletions packages/backend/src/plugins/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<
Expand Down Expand Up @@ -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 };
}
Expand All @@ -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(),
});
Expand All @@ -113,13 +111,51 @@ 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<string>(
data.map((cl: { metadata: { name: string } }) => cl.metadata.name),
);
}

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<string[] | undefined> {
Expand All @@ -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);
}
}
}

Expand All @@ -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', {
Expand All @@ -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;
}
}

Expand All @@ -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(
Expand Down
42 changes: 42 additions & 0 deletions packages/backend/src/premissions/repository.rule.ts
Original file line number Diff line number Diff line change
@@ -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,
);
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export async function createRouter(
},
);


const middleware = MiddlewareFactory.create({ logger, config });

router.use(middleware.error());
Expand Down
1 change: 1 addition & 0 deletions plugins/jenkins-with-reporting/src/api/JenkinsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down

0 comments on commit fb02501

Please sign in to comment.