Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 103 additions & 10 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,119 @@ on:
- '**'
env:
NODE_VERSION: "20"
OPENAPI_FILE: 'index.yaml'
PORT: '3000'

jobs:
node-ci:
lint:
runs-on: ubuntu-latest
steps:
# 1. Checkout repository
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci --prefer-offline --no-audit --no-fund


- name: Ensure minimal ESLint config
run: |
node -e "const fs=require('fs');const has=['.eslintrc.json','.eslintrc.js','eslint.config.js'].some(f=>fs.existsSync(f));if(!has){fs.writeFileSync('.eslintrc.json',JSON.stringify({root:true,env:{es2021:true,node:true,browser:true},parserOptions:{ecmaVersion:2022,sourceType:'module'},ignorePatterns:['node_modules/','dist/','build/','coverage/'],rules:{curly:'off',eqeqeq:'off','no-undef':'off','no-unused-vars':'off'}},null,2));}"

- name: Run ESLint (non-blocking)
run: npx -y eslint@8.57.0 . --no-error-on-unmatched-pattern --quiet || true

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci --prefer-offline --no-audit --no-fund


- name: Run fast tests (non-blocking)
shell: bash
run: |
set -e
if node -e "const s=(require('./package.json').scripts)||{};process.exit(s['test:unit']?0:1)"; then
npm run test:unit -- --ci || true
elif node -e "const p=require('./package.json');const hasJest=(p.devDependencies&&p.devDependencies.jest)||(p.dependencies&&p.dependencies.jest);process.exit(hasJest?0:1)"; then
npx jest --ci --passWithNoTests || true
else
npm test --silent || true
fi

openapi-validate:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

# 2. Setup Node.js with caching enabled
- name: Setup Node with cache
uses: actions/setup-node@v4
with:

node-version: ${{ env.NODE_VERSION }}

cache: npm

cache-dependency-path: package-lock.json

# 3. Install dependencies (reproducible and cache-friendly)
- name: Install dependencies
run: npm ci
- run: npm ci

- name: Install swagger-cli

run: npx -y swagger-cli@4.0.4 --version



- name: Validate OpenAPI (non-blocking)

run: |

REPORT=openapi-validate.log

if [ ! -f "${{ env.OPENAPI_FILE }}" ]; then

echo "::warning::OpenAPI file '${{ env.OPENAPI_FILE }}' not found at repo root. Skipping validation." | tee "$REPORT"

exit 0

fi

# Run validation, capture output; do not fail the step

npx swagger-cli validate "${{ env.OPENAPI_FILE }}" >"$REPORT" 2>&1 || {

echo "::warning::OpenAPI validation failed. See artifact '$REPORT' for details."

}

# Always succeed

exit 0

- name: Upload OpenAPI validation report

uses: actions/upload-artifact@v4

with:

name: openapi-validate-report

path: openapi-validate.log

if-no-files-found: ignore



run-security-scan:
runs-on: ubuntu-latest

Expand Down
42 changes: 35 additions & 7 deletions middleware/authorizeRoles.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,64 @@
/**
* Role-based access control (RBAC) middleware
* Role-based access control (RBAC) middleware with violation logging
*/
const { createClient } = require('@supabase/supabase-js');

const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY // ✅ still using anon key
);

function authorizeRoles(...allowedRoles) {
return (req, res, next) => {
// Supabase JWTs: "role" is usually "authenticated" or "service_role"
// Custom JWTs: you explicitly set "role" in payload
return async (req, res, next) => {
const userRole = req.user?.role || req.user?.user_roles || null;

if (!userRole) {
await logViolation(req, userRole, "ROLE_MISSING");
return res.status(403).json({
success: false,
error: "Role missing in token",
code: "ROLE_MISSING"
});
}

// Normalize role value (lowercase string)
const roleValue = String(userRole).toLowerCase();

// Normalize allowed roles too
const normalizedAllowed = allowedRoles.map(r => r.toLowerCase());

if (!normalizedAllowed.includes(roleValue)) {
await logViolation(req, roleValue, "ACCESS_DENIED");
return res.status(403).json({
success: false,
error: "Access denied: insufficient role",
code: "ACCESS_DENIED"
});
}

// ✅ If role is allowed, continue
next();
};
}
//feature/rbac-extension
async function logViolation(req, role, status) {
const payload = {
user_id: req.user?.userId || "unknown",
email: req.user?.email || "unknown", // ✅ added email
role: role || "unknown",
endpoint: req.originalUrl,
method: req.method,
status
};

try {
const { error } = await supabase.from("rbac_violation_logs").insert([payload]);
if (error) {
console.error("❌ Supabase insert error:", error.message);
} else {
console.log("✅ RBAC violation logged:", payload);
}
} catch (err) {
console.error("❌ RBAC log exception:", err.message);
}
}

module.exports = authorizeRoles;

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading