diff --git a/.dockerignore b/.dockerignore
index cf5b80a..da554c4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,5 @@
node_modules
#packages
!packages/backend/dist
-plugins
.env.local*
*.pem
diff --git a/Dockerfile b/Dockerfile
index fceb4bd..b60f3c2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,7 @@ COPY package.json yarn.lock ./
COPY packages packages
COPY patches patches
# Comment this out if you don't have any internal plugins
-#COPY plugins plugins
+COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+
@@ -51,8 +51,10 @@ COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/sk
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
COPY --from=packages /app .
+COPY ./plugins ./plugins
RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)"
COPY ./patches ./patches
+
RUN yarn run postinstall
# Copy the built packages from the build stage
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 3055213..918cbb9 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -8,6 +8,7 @@ import { ThemeProvider } from '@material-ui/core/styles';
import { BackstageOverrides } from '@backstage/core-components';
import loginBg from './assets/images/login-bg.jpg';
import sfLogoMinimal from './assets/images/sf-minimal-logo.png';
+import { PermissionWrapper } from './PermissionWrapper';
import {
CatalogEntityPage,
@@ -393,6 +394,14 @@ const routes = (
} />
+
+
+
+ }
+ />
} />
{ // NOSONAR
+ const hasPermission = usePermissionCheck(permission as string); // NOSONAR
+
+ if (hasPermission === null) {
+ return Loading...
; // Render a loading state while checking permissions
+ }
+
+ if (!hasPermission) {
+ return Permission Denied......
; // Render a "Permission Denied" message
+ }
+
+ return children;
+};
diff --git a/packages/app/src/usePermissionCheck.tsx b/packages/app/src/usePermissionCheck.tsx
new file mode 100644
index 0000000..f0d09c7
--- /dev/null
+++ b/packages/app/src/usePermissionCheck.tsx
@@ -0,0 +1,41 @@
+// usePermissionCheck.js
+
+import { useEffect, useState } from 'react';
+import {
+ useApi,
+ identityApiRef,
+ configApiRef,
+} from '@backstage/core-plugin-api';
+
+export const usePermissionCheck = (permission: string) => {
+ const config = useApi(configApiRef);
+ const [hasPermission, setHasPermission] = useState(null);
+ const identityApi = useApi(identityApiRef);
+
+ useEffect(() => {
+ const checkPermission = async () => {
+ const authDetail = await identityApi.getCredentials();
+ const backendUrl = config.getOptionalString('backend.baseUrl');
+ const response = await fetch(
+ `${backendUrl}/api/validate-access/validateuser`,
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ authorization: authDetail.token as string,
+ },
+ },
+ );
+ if (response.ok) {
+ const { allowed } = await response.json();
+ setHasPermission(allowed);
+ } else {
+ setHasPermission(null);
+ }
+ };
+
+ checkPermission();
+ }, [config, identityApi, permission]);
+
+ return hasPermission;
+};
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index c4736a5..a170f5b 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -31,6 +31,7 @@ import search from './plugins/search';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
+import validateAccessPlugin from '@internal/plugin-validate-access-backend';
function makeCreateEnv(config: Config) {
const root = getRootLogger();
@@ -85,6 +86,9 @@ async function main() {
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
+ const validateAccessPluginEnv = useHotMemoize(module, () =>
+ createEnv('validateAccessPlugin'),
+ );
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -93,6 +97,10 @@ async function main() {
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/search', await search(searchEnv));
+ apiRouter.use(
+ '/validate-access',
+ await validateAccessPlugin(validateAccessPluginEnv),
+ );
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
apiRouter.use(notFoundHandler());
diff --git a/plugins/validate-access-backend/.eslintrc.js b/plugins/validate-access-backend/.eslintrc.js
new file mode 100644
index 0000000..e2a53a6
--- /dev/null
+++ b/plugins/validate-access-backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/validate-access-backend/README.md b/plugins/validate-access-backend/README.md
new file mode 100644
index 0000000..2106379
--- /dev/null
+++ b/plugins/validate-access-backend/README.md
@@ -0,0 +1,15 @@
+# validate-access
+
+Welcome to the validate-access backend plugin!
+
+_This plugin was created through the Backstage CLI_
+This plugin used to validate users access. Ex: To check if the user is part of the team who can create, manage, delete github repos.
+
+## Getting started
+
+Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
+start` in the root directory, and then navigating to [/validate-access](http://localhost:3000/validate-access).
+
+You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
+This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
+It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
diff --git a/plugins/validate-access-backend/package.json b/plugins/validate-access-backend/package.json
new file mode 100644
index 0000000..53a370e
--- /dev/null
+++ b/plugins/validate-access-backend/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@internal/plugin-validate-access-backend",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "^0.18.5",
+ "@backstage/backend-plugin-api": "^0.6.18",
+ "@backstage/backend-tasks": "^0.5.23",
+ "@backstage/config": "^1.0.7",
+ "@backstage/core-plugin-api": "^1.9.2",
+ "@backstage/plugin-auth-node": "^0.4.13",
+ "@backstage/plugin-permission-common": "^0.7.13",
+ "@types/express": "*",
+ "axios": "^1.7.2",
+ "express": "^4.17.1",
+ "express-promise-router": "^4.1.0",
+ "jose": "^5.3.0",
+ "node-fetch": "^2.6.7",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^0.22.7",
+ "@types/supertest": "^2.0.12",
+ "msw": "^1.0.0",
+ "supertest": "^6.2.4"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/validate-access-backend/src/index.ts b/plugins/validate-access-backend/src/index.ts
new file mode 100644
index 0000000..79a5f55
--- /dev/null
+++ b/plugins/validate-access-backend/src/index.ts
@@ -0,0 +1,9 @@
+export * from './service/router';
+import { createRouter } from './service/router';
+import { PluginEnvironment } from './types';
+
+export default async function createPlugin(env: PluginEnvironment) {
+ return await createRouter({
+ logger: env.logger,
+ });
+}
diff --git a/plugins/validate-access-backend/src/plugin.ts b/plugins/validate-access-backend/src/plugin.ts
new file mode 100644
index 0000000..09b4fe2
--- /dev/null
+++ b/plugins/validate-access-backend/src/plugin.ts
@@ -0,0 +1,8 @@
+import { createRouter } from './service/router';
+import { PluginEnvironment } from './types';
+
+export default async function createPlugin(env: PluginEnvironment) {
+ return await createRouter({
+ logger: env.logger,
+ });
+}
diff --git a/plugins/validate-access-backend/src/run.ts b/plugins/validate-access-backend/src/run.ts
new file mode 100644
index 0000000..d0c72b8
--- /dev/null
+++ b/plugins/validate-access-backend/src/run.ts
@@ -0,0 +1,21 @@
+import { getRootLogger } from '@backstage/backend-common';
+import yn from 'yn';
+import { startStandaloneServer } from './service/standaloneServer';
+
+const DEFAULT_PORT = 7007;
+
+const port = process.env.PLUGIN_PORT
+ ? Number(process.env.PLUGIN_PORT)
+ : DEFAULT_PORT;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
+const logger = getRootLogger();
+
+startStandaloneServer({ port, enableCors, logger }).catch(err => {
+ logger.error(err);
+ process.exit(1);
+});
+
+process.on('SIGINT', () => {
+ logger.info('CTRL+C pressed; exiting.');
+ process.exit(0);
+});
diff --git a/plugins/validate-access-backend/src/service/router.test.ts b/plugins/validate-access-backend/src/service/router.test.ts
new file mode 100644
index 0000000..4d55e7e
--- /dev/null
+++ b/plugins/validate-access-backend/src/service/router.test.ts
@@ -0,0 +1,31 @@
+import { getVoidLogger } from '@backstage/backend-common';
+import express from 'express';
+import request from 'supertest';
+
+import { createRouter } from './router';
+
+const STATUS_OK = 200;
+
+describe('createRouter', () => {
+ let app: express.Express;
+
+ beforeAll(async () => {
+ const router = await createRouter({
+ logger: getVoidLogger(),
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /health', () => {
+ it('returns ok', async () => {
+ const response = await request(app).get('/health');
+
+ expect(response.status).toEqual(STATUS_OK);
+ expect(response.body).toEqual({ status: 'ok' });
+ });
+ });
+});
diff --git a/plugins/validate-access-backend/src/service/router.ts b/plugins/validate-access-backend/src/service/router.ts
new file mode 100644
index 0000000..bb7112b
--- /dev/null
+++ b/plugins/validate-access-backend/src/service/router.ts
@@ -0,0 +1,39 @@
+import { errorHandler } from '@backstage/backend-common';
+import express from 'express';
+import Router from 'express-promise-router';
+import { Logger } from 'winston';
+import { isUserAllowed } from './validateRepositoryManager';
+import * as jose from 'jose';
+
+export interface RouterOptions {
+ logger: Logger;
+}
+
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger } = options;
+
+ const router = Router();
+ router.use(express.json());
+
+ router.get('/health', (_, response) => {
+ logger.info('PONG!');
+ response.json({ status: 'ok' });
+ });
+
+ router.get('/validateuser', async (_, res) => {
+ const token = _.headers?.authorization as string;
+ if (token !== '') {
+ const userIdentityDetails = jose.decodeJwt(token);
+ const userAllowed = await isUserAllowed(
+ userIdentityDetails?.sub?.split('/')[1] as string,
+ );
+ res.json({ allowed: userAllowed });
+ }
+ res.json({ allowed: false });
+ });
+
+ router.use(errorHandler());
+ return router;
+}
diff --git a/plugins/validate-access-backend/src/service/standaloneServer.ts b/plugins/validate-access-backend/src/service/standaloneServer.ts
new file mode 100644
index 0000000..009bb76
--- /dev/null
+++ b/plugins/validate-access-backend/src/service/standaloneServer.ts
@@ -0,0 +1,34 @@
+import { createServiceBuilder } from '@backstage/backend-common';
+import { Server } from 'http';
+import { Logger } from 'winston';
+import { createRouter } from './router';
+
+export interface ServerOptions {
+ port: number;
+ enableCors: boolean;
+ logger: Logger;
+}
+
+export async function startStandaloneServer(
+ options: ServerOptions
+): Promise {
+ const logger = options.logger.child({ service: 'my-backend-plugin-backend' });
+ logger.debug('Starting application server...');
+ const router = await createRouter({
+ logger,
+ });
+
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
+ .addRouter('/validate-access-plugin', router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: 'http://localhost:3000' });
+ }
+
+ return await service.start().catch(err => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/plugins/validate-access-backend/src/service/validateRepositoryManager.ts b/plugins/validate-access-backend/src/service/validateRepositoryManager.ts
new file mode 100644
index 0000000..fcdd86e
--- /dev/null
+++ b/plugins/validate-access-backend/src/service/validateRepositoryManager.ts
@@ -0,0 +1,27 @@
+import axios from 'axios';
+
+// Function to check if a user is allowed
+export const isUserAllowed = async (
+ user: string
+): Promise => {
+ try {
+ const token = process.env.GITHUB_TOKEN; // GitHub token with appropriate permissions
+ const response = await axios.get(
+ `${process.env.GITHUB_API_URL}/orgs/${process.env.GITHUB_ORGANIZATION}/teams/${process.env.REPO_CREATOR_TEAM}/memberships/${user}`,
+ {
+ headers: {
+ Authorization: `token ${token}`,
+ },
+ },
+ );
+
+ const userAccess = response.data;
+ return (
+ userAccess.role &&
+ ['member', 'admin'].includes(userAccess.role) &&
+ userAccess.state === 'active'
+ );
+ } catch (error) {
+ return false;
+ }
+};
diff --git a/plugins/validate-access-backend/src/setupTests.ts b/plugins/validate-access-backend/src/setupTests.ts
new file mode 100644
index 0000000..cb0ff5c
--- /dev/null
+++ b/plugins/validate-access-backend/src/setupTests.ts
@@ -0,0 +1 @@
+export {};
diff --git a/plugins/validate-access-backend/src/types.ts b/plugins/validate-access-backend/src/types.ts
new file mode 100644
index 0000000..9cd2c74
--- /dev/null
+++ b/plugins/validate-access-backend/src/types.ts
@@ -0,0 +1,25 @@
+import { Logger } from 'winston';
+import { Config } from '@backstage/config';
+import {
+ PluginCacheManager,
+ PluginDatabaseManager,
+ PluginEndpointDiscovery,
+ TokenManager,
+ UrlReader,
+} from '@backstage/backend-common';
+import { PluginTaskScheduler } from '@backstage/backend-tasks';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+
+export type PluginEnvironment = {
+ logger: Logger;
+ database: PluginDatabaseManager;
+ cache: PluginCacheManager;
+ config: Config;
+ reader: UrlReader;
+ discovery: PluginEndpointDiscovery;
+ tokenManager: TokenManager;
+ scheduler: PluginTaskScheduler;
+ permissions: PermissionEvaluator;
+ identity: IdentityApi;
+};
diff --git a/terraform/data.tf b/terraform/data.tf
index 39a5e8b..a5a7a07 100644
--- a/terraform/data.tf
+++ b/terraform/data.tf
@@ -34,3 +34,8 @@ data "aws_subnets" "private" {
]
}
}
+
+# Define data sources for SSM parameters
+data "aws_ssm_parameter" "repo_name" {
+ name = "/backstage/${var.environment}/repo/creator/name"
+}
diff --git a/terraform/locals.tf b/terraform/locals.tf
index 1a561b3..544d731 100644
--- a/terraform/locals.tf
+++ b/terraform/locals.tf
@@ -1,4 +1,18 @@
locals {
route_53_zone = trimprefix(var.acm_domain_name, "*.")
health_check_domain = "healthcheck-${var.namespace}-${var.environment}.${local.route_53_zone}"
+ environment_variables = [
+ {
+ name = "GITHUB_API_URL"
+ value = "https://api.github.com"
+ },
+ {
+ name = "GITHUB_ORGANIZATION"
+ value = "sourcefuse"
+ },
+ {
+ name = "REPO_CREATOR_TEAM"
+ value = data.aws_ssm_parameter.repo_name.value
+ },
+ ]
}
diff --git a/terraform/main.tf b/terraform/main.tf
index 624cec4..a130f59 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -45,7 +45,7 @@ module "ecs_common_data" {
################################################################################
module "backstage" {
source = "sourcefuse/arc-backstage-ecs-app/aws"
- version = "0.2.6"
+ version = "0.2.7"
alb_dns_name = module.ecs_common_data.alb_dns_name
alb_zone_id = module.ecs_common_data.alb_dns_zone_id
app_host_name = var.app_host_name
@@ -66,4 +66,5 @@ module "backstage" {
private_key_secret_name = var.private_key_secret_name
health_check_path_pattern = "/healthcheck"
desired_count = 2
+ environment_variables = local.environment_variables
}