Skip to content

Commit f5b26e1

Browse files
committed
🔀Merge branch 'keycloak'
2 parents 2a36f5d + e931570 commit f5b26e1

File tree

106 files changed

+3865
-1251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+3865
-1251
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Create and publish Docker image for keycloak for controlpanel to ghcr.io
2+
3+
on:
4+
release:
5+
types: ['published']
6+
7+
env:
8+
REGISTRY: ghcr.io
9+
IMAGE_NAME: SAP/keycloak-controlpanel
10+
11+
jobs:
12+
build-and-push-image:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
packages: write
17+
attestations: write
18+
id-token: write
19+
20+
environment: ghcr:cloud-active-defense
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Log in to the Container registry
27+
uses: docker/login-action@v3.2.0
28+
with:
29+
registry: ${{ env.REGISTRY }}
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Extract metadata of keycloak for controlpanel
34+
id: meta
35+
uses: docker/metadata-action@v5.5.1
36+
with:
37+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
38+
39+
- name: Build and push Docker image of keycloak for controlpanel
40+
id: push
41+
uses: docker/build-push-action@v5.3.0
42+
with:
43+
context: ./keycloak
44+
push: true
45+
file: ./keycloak/Dockerfile
46+
tags: ${{ steps.meta.outputs.tags }}
47+
labels: ${{ steps.meta.outputs.labels }}

assets/keycloak-register.png

454 KB
Loading

controlpanel/api/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ DB_PORT=5432
55

66
CONTROLPANEL_FRONTEND_URL=http://localhost:4200
77
DEPLOYMENT_MANAGER_URL=
8+
KEYCLOAK_URL=http://host.docker.internal:8080
89

910
***REMOVED***
1011

12+
***REMOVED***
1113
***REMOVED***
1214
***REMOVED***
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const Keycloak = require('keycloak-connect');
2+
require('dotenv').config({ path: __dirname + '/.env' });
3+
4+
const keycloak = new Keycloak({}, {
5+
"realm": "cad",
6+
"bearer-only": true,
7+
"auth-server-url": process.env.KEYCLOAK_URL,
8+
"ssl-required": "external",
9+
"resource": "cad"
10+
});
11+
12+
module.exports = keycloak;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const Customer = require('../models/Customer');
2+
const ProtectedApp = require('../models/ProtectedApp');
3+
const Decoy = require('../models/Decoy-data');
4+
5+
const authorizationFromPa_id = async (req, res, next) => {
6+
const token = req.headers['authorization']?.split(' ')[1];
7+
if (!token) return res.status(401).json({ code: 401, type: 'error', message: 'Unauthorized' });
8+
9+
const customer_id = await extractCustomersFromToken(token);
10+
if (!customer_id) return res.status(401).json({ code: 401, type: 'error', message: 'Invalid authorization token' });
11+
12+
const pa_id = req.params.pa_id || (req.body && req.body.pa_id);
13+
if (!pa_id) return res.status(400).json({ code: 400, type: 'error', message: 'Protected app ID is missing' });
14+
15+
const protectedApp = await ProtectedApp.findOne({ where: { id: pa_id }, include: [{model: Customer, as: 'customer'}]});
16+
if (!protectedApp) return res.status(404).json({ code: 404, type: 'error', message: 'Protected app not found' });
17+
if (!customer_id == protectedApp.customer.id) return res.status(403).json({ code: 403, type: 'error', message: 'Forbidden' });
18+
next();
19+
}
20+
const authorizationFromCu_id = async (req, res, next) => {
21+
const token = req.headers['authorization']?.split(' ')[1];
22+
if (!token) return res.status(401).json({ code: 401, type: 'error', message: 'Unauthorized' });
23+
24+
const customer_id = await extractCustomersFromToken(token);
25+
if (!customer_id) return res.status(401).json({ code: 401, type: 'error', message: 'Invalid authorization token' });
26+
req.cu_id = customer_id;
27+
next();
28+
}
29+
const authorizationFromDecoyId = async (req, res, next) => {
30+
const token = req.headers['authorization']?.split(' ')[1];
31+
if (!token) return res.status(401).json({ code: 401, type: 'error', message: 'Unauthorized' });
32+
33+
const customer_id = await extractCustomersFromToken(token);
34+
if (!customer_id) return res.status(401).json({ code: 401, type: 'error', message: 'Invalid authorization token' });
35+
36+
const decoyId = req.params.id || (req.body && req.body.id);
37+
if (!decoyId) return res.status(400).json({ code: 400, type: 'error', message: 'Decoy ID is missing' });
38+
39+
const decoy = await Decoy.findOne({ where: { id: decoyId }, include: [{ model: ProtectedApp, as: 'protectedApp' }] });
40+
if (!decoy) return res.status(404).json({ code: 404, type: 'error', message: 'Decoy not found' });
41+
42+
if (!customer_id == decoy.protectedApp.cu_id) return res.status(403).json({ code: 403, type: 'error', message: 'Forbidden' });
43+
next();
44+
}
45+
46+
const extractCustomersFromToken = async (token) => {
47+
const parts = token.split('.');
48+
if (parts.length !== 3) return null;
49+
50+
const payload = Buffer.from(parts[1], 'base64').toString('utf8');
51+
const data = JSON.parse(payload);
52+
53+
if (!data || !data.groups) return null;
54+
55+
const customer = await Customer.findOne({ where: { name: data.groups }, attributes: ['id']});
56+
if (!customer) return null;
57+
return customer.id;
58+
}
59+
60+
module.exports = {
61+
authorizationFromPa_id,
62+
authorizationFromCu_id,
63+
authorizationFromDecoyId
64+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const ApiKey = require('../models/Api-key');
2+
3+
const authenticate = async (req, res, next) => {
4+
const key = req.headers['authorization'];
5+
if (!key) {
6+
return res.status(401).json({ type: 'error', code: 401, message: "You must use an API key to access this" });
7+
}
8+
const apiKey = await ApiKey.findOne({ where: { key }})
9+
if (!apiKey) {
10+
return res.status(403).json({ type: 'error', code: 403, message: "Invalid API key" });
11+
}
12+
if (!apiKey.permissions.includes('keycloak') && !apiKey.permissions.includes('admin')) {
13+
return res.status(403).json({ type: 'error', code: 403, message: "You don't have the required permissions to access this" });
14+
}
15+
next();
16+
};
17+
18+
module.exports = authenticate;

controlpanel/api/models/Api-key.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const ApiKey = sequelize.define("apiKey", {
1818
},
1919
pa_id: {
2020
type: DataTypes.UUID,
21-
allowNull: false,
2221
references: {
2322
model: ProtectedApp,
2423
key: 'id'

controlpanel/api/models/index.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const Logs = require("./Logs");
77
const ApiKey = require("./Api-key");
88
const Customer = require("./Customer");
99

10+
let dbInitialized = false;
11+
1012
async function initializeDatabase() {
1113
await sequelize.sync();
1214

@@ -26,26 +28,31 @@ async function initializeDatabase() {
2628
GRANT SELECT ON TABLE customers TO deployment_manager;
2729
`, { replacements: { password }});
2830
}
31+
dbInitialized = true;
2932
console.log("Database connected successfully.");
3033
} catch (error) {
3134
console.error("Unable to connect to the database...\n", error);
3235
throw error;
3336
}
3437
}
3538

36-
Decoy.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApps'});
37-
ProtectedApp.hasMany(Decoy, {foreignKey: 'pa_id', as: 'decoys' });
39+
function isInitialized() {
40+
return dbInitialized;
41+
}
42+
43+
Decoy.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApp', onDelete: 'CASCADE'});
44+
ProtectedApp.hasMany(Decoy, {foreignKey: 'pa_id', as: 'decoys', onDelete: 'CASCADE'});
3845

39-
Config.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApps'});
40-
ProtectedApp.hasMany(Config, {foreignKey: 'pa_id', as: 'configs' });
46+
Config.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApp', onDelete: 'CASCADE'});
47+
ProtectedApp.hasMany(Config, {foreignKey: 'pa_id', as: 'configs', onDelete: 'CASCADE'});
4148

42-
Logs.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApps'});
43-
ProtectedApp.hasMany(Logs, {foreignKey: 'pa_id', as: 'logs' });
49+
Logs.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApp', onDelete: 'CASCADE'});
50+
ProtectedApp.hasMany(Logs, {foreignKey: 'pa_id', as: 'logs', onDelete: 'CASCADE'});
4451

45-
ApiKey.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedapps'});
46-
ProtectedApp.hasMany(ApiKey, {foreignKey: 'pa_id', as: 'apiKeys' });
52+
ApiKey.belongsTo(ProtectedApp, {foreignKey: 'pa_id', as: 'protectedApp', onDelete: 'CASCADE'});
53+
ProtectedApp.hasMany(ApiKey, {foreignKey: 'pa_id', as: 'apiKeys', onDelete: 'CASCADE'});
4754

48-
ProtectedApp.belongsTo(Customer, {foreignKey: 'cu_id', as: 'customers' });
55+
ProtectedApp.belongsTo(Customer, {foreignKey: 'cu_id', as: 'customer' });
4956
Customer.hasMany(ProtectedApp, {foreignKey: 'cu_id', as: 'protectedApps' });
5057

51-
module.exports = { sequelize, initializeDatabase };
58+
module.exports = { sequelize, initializeDatabase, isInitialized };

0 commit comments

Comments
 (0)