First commit
This commit is contained in:
8
apps/api-bff/src/config.ts
Normal file
8
apps/api-bff/src/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const config = {
|
||||
env: (process && process.env && process.env.NODE_ENV) || 'development',
|
||||
port: Number(process?.env?.PORT) || 4000,
|
||||
host: process?.env?.HOST || '0.0.0.0',
|
||||
version: '0.0.0',
|
||||
cors: { allowedOrigins: ['*'] },
|
||||
permitio: { pdpUrl: process?.env?.PERMIT_PDP_URL || '', apiKey: process?.env?.PERMIT_API_KEY || '' }
|
||||
};
|
||||
5
apps/api-bff/src/context.ts
Normal file
5
apps/api-bff/src/context.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Context = any;
|
||||
|
||||
export function createContext({ request, reply }: { request?: any; reply?: any }): Context {
|
||||
return { request, reply } as Context;
|
||||
}
|
||||
5
apps/api-bff/src/graphql/resolvers.ts
Normal file
5
apps/api-bff/src/graphql/resolvers.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
_empty: () => 'ok'
|
||||
}
|
||||
};
|
||||
3
apps/api-bff/src/graphql/schema.ts
Normal file
3
apps/api-bff/src/graphql/schema.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const typeDefs = `
|
||||
type Query { _empty: String }
|
||||
`;
|
||||
9
apps/api-bff/src/main.ts
Normal file
9
apps/api-bff/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import express from "express";
|
||||
import projectsRouter from "./routes/projects";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.use("/api", projectsRouter);
|
||||
|
||||
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
|
||||
6
apps/api-bff/src/middleware/auth.ts
Normal file
6
apps/api-bff/src/middleware/auth.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const authMiddleware = async (request: any, reply: any) => {
|
||||
// stub: authenticate request and attach `user` to request in real implementation
|
||||
request.user = request.user || { sub: 'anonymous' };
|
||||
};
|
||||
|
||||
export default authMiddleware;
|
||||
5
apps/api-bff/src/middleware/dpop.ts
Normal file
5
apps/api-bff/src/middleware/dpop.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const dpopMiddleware = async (request: any, reply: any) => {
|
||||
// stub: validate DPoP headers in real implementation
|
||||
};
|
||||
|
||||
export default dpopMiddleware;
|
||||
6
apps/api-bff/src/middleware/policy.ts
Normal file
6
apps/api-bff/src/middleware/policy.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const policyMiddleware = async (request: any, reply: any) => {
|
||||
// stub: evaluate ABAC policies; attach decisions to request
|
||||
request.policy = { allowed: true };
|
||||
};
|
||||
|
||||
export default policyMiddleware;
|
||||
57
apps/api-bff/src/middleware/requireAccess.ts
Normal file
57
apps/api-bff/src/middleware/requireAccess.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { checkAccess } from "../services/authz";
|
||||
|
||||
type CrudAction = "create" | "read" | "update" | "delete" | "admin" | "view";
|
||||
|
||||
function isNonEmptyString(v: unknown): v is string {
|
||||
return typeof v === "string" && v.trim().length > 0;
|
||||
}
|
||||
|
||||
export function requireAccess(
|
||||
getConfig: (req: Request) => {
|
||||
userId: string;
|
||||
resourceName: string;
|
||||
action: CrudAction;
|
||||
}
|
||||
) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const cfg = getConfig(req);
|
||||
|
||||
const userId = cfg?.userId;
|
||||
const resourceName = cfg?.resourceName;
|
||||
const action = cfg?.action;
|
||||
|
||||
// ✅ Validate early (prevents calling OPA with undefined/null)
|
||||
if (!isNonEmptyString(userId) || !isNonEmptyString(resourceName) || !isNonEmptyString(action)) {
|
||||
return res.status(400).json({
|
||||
message: "Missing or invalid authorization inputs",
|
||||
details: {
|
||||
userId: userId ?? null,
|
||||
resourceName: resourceName ?? null,
|
||||
action: action ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("AUTHZ INPUT:", { userId, resourceName, action });
|
||||
|
||||
// checkAccess MUST build:
|
||||
// { input: { user: {id:userId}, resource:{name:resourceName}, action } }
|
||||
const allowed = await checkAccess({ userId, resourceName, action });
|
||||
|
||||
console.log("AUTHZ RESULT:", allowed);
|
||||
|
||||
if (!allowed) {
|
||||
return res.status(403).json({ message: "Forbidden: access denied" });
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (e: any) {
|
||||
return res.status(502).json({
|
||||
message: "Authorization service error",
|
||||
error: e?.message ?? "Unknown error",
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
5
apps/api-bff/src/plugins/rateLimit.ts
Normal file
5
apps/api-bff/src/plugins/rateLimit.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const rateLimitPlugin = async (server: any, opts: any) => {
|
||||
// stub: register rate limiting hooks
|
||||
};
|
||||
|
||||
export default rateLimitPlugin;
|
||||
5
apps/api-bff/src/plugins/security.ts
Normal file
5
apps/api-bff/src/plugins/security.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const securityPlugin = async (server: any, opts: any) => {
|
||||
// stub: set security-related Fastify hooks
|
||||
};
|
||||
|
||||
export default securityPlugin;
|
||||
58
apps/api-bff/src/routes/projects.ts
Normal file
58
apps/api-bff/src/routes/projects.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Router } from "express";
|
||||
import { requireAccess } from "../middleware/requireAccess";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// CREATE (uses payload values)
|
||||
router.post(
|
||||
"/projects",
|
||||
requireAccess((req) => ({
|
||||
userId: req.body.userId,
|
||||
resourceName: req.body.resourceName,
|
||||
action: req.body.action,
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json({ message: "Project created" });
|
||||
}
|
||||
);
|
||||
|
||||
// READ (if you still want body-based auth)
|
||||
router.get(
|
||||
"/projects/:name",
|
||||
requireAccess((req) => ({
|
||||
userId: req.body.userId,
|
||||
resourceName: req.body.resourceName ?? req.params.name,
|
||||
action: req.body.action ?? "read",
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json({ name: req.params.name });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE (body-based)
|
||||
router.put(
|
||||
"/projects/:name",
|
||||
requireAccess((req) => ({
|
||||
userId: req.body.userId,
|
||||
resourceName: req.body.resourceName ?? req.params.name,
|
||||
action: req.body.action ?? "update",
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json({ message: "Project updated" });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE (body-based)
|
||||
router.delete(
|
||||
"/projects/:name",
|
||||
requireAccess((req) => ({
|
||||
userId: req.body.userId,
|
||||
resourceName: req.body.resourceName ?? req.params.name,
|
||||
action: req.body.action ?? "delete",
|
||||
})),
|
||||
async (req, res) => {
|
||||
res.json({ message: "Project deleted" });
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
15
apps/api-bff/src/services/authz.ts
Normal file
15
apps/api-bff/src/services/authz.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { opaAllow } from "./opaClient";
|
||||
|
||||
export async function checkAccess(params: {
|
||||
userId: string;
|
||||
resourceName: string;
|
||||
action: string;
|
||||
}): Promise<boolean> {
|
||||
return opaAllow({
|
||||
input: {
|
||||
user: { id: params.userId },
|
||||
resource: { name: params.resourceName },
|
||||
action: params.action,
|
||||
},
|
||||
});
|
||||
}
|
||||
16
apps/api-bff/src/services/opaClient.ts
Normal file
16
apps/api-bff/src/services/opaClient.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
import { AccessRequest, AccessResponse } from "../types/authz";
|
||||
|
||||
const OPA_URL = "http://64.227.108.180:8182/v1/data/authz/access/allow";
|
||||
|
||||
export async function opaAllow(payload: AccessRequest): Promise<boolean> {
|
||||
console.log("OPA REQUEST:", JSON.stringify(payload, null, 2));
|
||||
|
||||
const res = await axios.post<AccessResponse>(OPA_URL, payload, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
console.log("OPA RESPONSE:", JSON.stringify(res.data, null, 2));
|
||||
return Boolean(res.data?.result);
|
||||
}
|
||||
38
apps/api-bff/src/shims.d.ts
vendored
Normal file
38
apps/api-bff/src/shims.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Minimal external module shims for static analysis. Keep these permissive. */
|
||||
|
||||
declare module 'fastify' {
|
||||
const Fastify: any;
|
||||
export type FastifyInstance = any;
|
||||
export default Fastify;
|
||||
}
|
||||
|
||||
declare module '@apollo/server' {
|
||||
export class ApolloServer<T = any> {
|
||||
constructor(options?: any);
|
||||
start?(): Promise<void>;
|
||||
stop?(): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@as-integrations/fastify' {
|
||||
const fastifyApollo: any;
|
||||
export default fastifyApollo;
|
||||
export const fastifyApolloDrainPlugin: any;
|
||||
}
|
||||
|
||||
declare module '@apollo/server-plugin-landing-page-local-default' {
|
||||
export const ApolloServerPluginLandingPageLocalDefault: any;
|
||||
}
|
||||
|
||||
declare module '@graphql-tools/schema' {
|
||||
export function makeExecutableSchema(...args: any[]): any;
|
||||
}
|
||||
|
||||
declare module '@permitio/permit-node' {
|
||||
export class Permit { constructor(opts?: any); check(...args: any[]): Promise<any>; }
|
||||
}
|
||||
|
||||
declare module '@fastify/cors';
|
||||
declare module '@fastify/helmet';
|
||||
|
||||
declare var process: any;
|
||||
12
apps/api-bff/src/simple.rego
Normal file
12
apps/api-bff/src/simple.rego
Normal file
@@ -0,0 +1,12 @@
|
||||
package permission
|
||||
|
||||
import data.role_permissions
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
some role in input.subject.roles
|
||||
some permission in role_permissions[role]
|
||||
permission.action == input.action
|
||||
permission.object == input.object
|
||||
}
|
||||
7
apps/api-bff/src/simple_allow_input.json
Normal file
7
apps/api-bff/src/simple_allow_input.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"subject": {
|
||||
"roles": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
}
|
||||
11
apps/api-bff/src/types/authz.ts
Normal file
11
apps/api-bff/src/types/authz.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface AccessRequest {
|
||||
input: {
|
||||
user: { id: string };
|
||||
resource: { name: string };
|
||||
action: string; // "admin" | "view" | "read" | "create" | etc.
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccessResponse {
|
||||
result: boolean;
|
||||
}
|
||||
1
apps/api-bff/src/utils/logger.ts
Normal file
1
apps/api-bff/src/utils/logger.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const logger = console;
|
||||
Reference in New Issue
Block a user