add auth functionality

This commit is contained in:
2025-12-08 14:28:07 +00:00
parent 2c0af8e237
commit 44cd64a1db
34 changed files with 3463 additions and 7056 deletions

View File

@@ -1,4 +1,4 @@
FROM node:20
FROM node:25
ADD src/package*.json /app/
WORKDIR /app
RUN npm install

View File

@@ -59,6 +59,14 @@ services:
# - Set up auth: https://console.cloud.google.com/auth/overview
# GOOGLE_CLIENT_ID:
# GOOGLE_CLIENT_SECRET:
# PDP MODULES
# ===========
PDP_TYPE: cerbos
# Cerbos
# -----------
PDP_URL: http://jlinc-pdp:3592
restart: unless-stopped
depends_on:
- jlinc-db
@@ -83,5 +91,16 @@ services:
networks:
- jlinc
jlinc-pdp:
image: cerbos/cerbos:0.47.0
container_name: jlinc-pdp
ports:
- 127.0.0.1:3592:3592
volumes:
- ./policies:/policies
restart: unless-stopped
networks:
- jlinc
networks:
jlinc:

41
policies/data.yaml Normal file
View File

@@ -0,0 +1,41 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: data
version: default
rules:
- actions:
- create
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- read
effect: EFFECT_ALLOW
roles:
- user
- admin
- thirdParty
- actions:
- update
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- admin
# This is an example of using conditions for attribute-based access control
# The action is only allowed if the principal ID matches the ownerId attribute
# - actions:
# - someAction
# effect: EFFECT_ALLOW
# roles:
# - user
# condition:
# match:
# expr: request.resource.attr.ownerId == request.principal.id

41
policies/data_test.yaml Normal file
View File

@@ -0,0 +1,41 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestSuite.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/compile#testing
name: dataTestSuite
description: Tests for verifying the data resource policy
tests:
- name: data actions
input:
principals:
- user#1
- admin#2
- thirdParty#3
resources:
- data#1
actions:
- create
- read
- update
- delete
expected:
- resource: data#1
principal: user#1
actions:
create: EFFECT_DENY
read: EFFECT_ALLOW
update: EFFECT_DENY
delete: EFFECT_DENY
- resource: data#1
principal: admin#2
actions:
create: EFFECT_ALLOW
read: EFFECT_ALLOW
update: EFFECT_ALLOW
delete: EFFECT_ALLOW
- resource: data#1
principal: thirdParty#3
actions:
create: EFFECT_DENY
read: EFFECT_ALLOW
update: EFFECT_DENY
delete: EFFECT_DENY

40
policies/privateData.yaml Normal file
View File

@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: privateData
version: default
rules:
- actions:
- create
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- read
effect: EFFECT_ALLOW
roles:
- admin
- user
- actions:
- update
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- admin
# This is an example of using conditions for attribute-based access control
# The action is only allowed if the principal ID matches the ownerId attribute
# - actions:
# - someAction
# effect: EFFECT_ALLOW
# roles:
# - admin
# condition:
# match:
# expr: request.resource.attr.ownerId == request.principal.id

View File

@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/Policy.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/resource_policies
apiVersion: api.cerbos.dev/v1
resourcePolicy:
resource: privateData
version: default
rules:
- actions:
- create
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- read
effect: EFFECT_ALLOW
roles:
- admin
- user
- actions:
- update
effect: EFFECT_ALLOW
roles:
- admin
- actions:
- delete
effect: EFFECT_ALLOW
roles:
- admin
# This is an example of using conditions for attribute-based access control
# The action is only allowed if the principal ID matches the ownerId attribute
# - actions:
# - someAction
# effect: EFFECT_ALLOW
# roles:
# - admin
# condition:
# match:
# expr: request.resource.attr.ownerId == request.principal.id

19
policies/testdata/principals.yaml vendored Normal file
View File

@@ -0,0 +1,19 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Principals.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/compile#_sharing_test_fixtures
principals:
user#1:
id: user#1
roles:
- user
attr: {}
admin#2:
id: admin#2
roles:
- admin
attr: {}
thirdParty#3:
id: thirdParty#3
roles:
- thirdParty
attr: {}

12
policies/testdata/resources.yaml vendored Normal file
View File

@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://api.cerbos.dev/latest/cerbos/policy/v1/TestFixture/Resources.schema.json
# docs: https://docs.cerbos.dev/cerbos/latest/policies/compile#_sharing_test_fixtures
resources:
data#1:
id: data#1
kind: data
attr: {}
privateData#2:
id: privateData#2
kind: privateData
attr: {}

View File

@@ -32,6 +32,8 @@ export async function loadConfig() {
const { getModuleConfig } = await import(appModulePath);
config.appModules[type] = getModuleConfig();
}
if (process.env.PDP_TYPE) config.pdpType = process.env.PDP_TYPE;
if (process.env.PDP_URL) config.pdpUrl = process.env.PDP_URL;
}
export function getConfig() {

View File

@@ -0,0 +1,4 @@
{
"purposes": [],
"prohibitions": []
}

View File

@@ -131,9 +131,12 @@ export async function populateAgreements() {
withFileTypes: true,
});
for await (const file of files) {
const agreementId = file.name.slice(0, 36);
if (file.name.endsWith('.json'))
continue;
const agreementUuid = file.name.slice(0, 36);
const title = file.name.slice(39, file.name.length - 3);
const markdown = fs.readFileSync(path.join("./db/agreements", file.name), "utf8").trim();
const json = JSON.parse(fs.readFileSync(path.join("./db/agreements", file.name.replace('.md', '.json')), "utf8").trim());
const hash = createHash('sha256')
.update(markdown)
.digest('hex')
@@ -141,40 +144,48 @@ export async function populateAgreements() {
const agreementExists = await client.query(`
SELECT
CASE
WHEN (SELECT COUNT(1) FROM agreement WHERE agreement_id_uuid = $1 AND user_id IS NULL) > 0
WHEN (SELECT COUNT(1) FROM agreement_content WHERE title = $1 AND hash = $2 AND user_id IS NULL) > 0
THEN TRUE
ELSE FALSE
END AS exists
`,
[
agreementId,
title,
hash,
]
);
if (!agreementExists.rows[0].exists) {
console.log(`Adding agreement '${title}' (${agreementId})`);
console.log(`Adding agreement '${title}' (${agreementUuid})`);
await client.query(`
INSERT INTO agreement_content (
title,
markdown,
hash
hash,
key,
agreement_uuid
) VALUES (
$1,
$2,
$3
) ON CONFLICT DO NOTHING;
$3,
$4,
$5
);
`, [
title,
markdown,
hash,
json.key,
agreementUuid,
]);
const agreement = {
"@context": json["@context"] || 'https://protocol.jlinc.org/context/jlinc-v7.jsonld',
uri: `${config.publicCoreUrl}/agreements/${hash}`,
purposes: [],
caveats: [],
purposes: json.purposes || [],
prohibitions: json.prohibitions || [],
shortNames: [],
validRoles: [],
validRoles: json.validRoles || [],
}
await data.agreement.create(agreement, null, client, agreementId);
await data.agreement.create(agreement, null, client, agreementUuid);
}
} catch (e) {
console.error(e);

View File

@@ -0,0 +1,46 @@
ALTER TABLE public.caveat RENAME TO prohibition;
ALTER TABLE public.prohibition RENAME CONSTRAINT uniq__caveat TO uniq__prohibition;
ALTER INDEX idx__public__caveat__user_id RENAME TO idx__public__prohibition__user_id;
ALTER INDEX idx__public__caveat__value RENAME TO idx__public__prohibition__value;
ALTER INDEX idx__public__caveat__created_ts RENAME TO idx__public__prohibition__created_ts;
ALTER INDEX idx__public__caveat__updated_ts RENAME TO idx__public__prohibition__updated_ts;
ALTER TABLE public.agreement_caveat RENAME TO agreement_prohibition;
ALTER TABLE public.agreement_prohibition RENAME CONSTRAINT uniq__agreement_caveat TO uniq__agreement_prohibition;
ALTER TABLE public.agreement_prohibition RENAME COLUMN caveat_id TO prohibition_id;
ALTER TABLE public.agreement_prohibition
DROP CONSTRAINT IF EXISTS agreement_caveat_caveat_id_fkey,
ADD CONSTRAINT agreement_prohibition_prohibition_id_fkey
FOREIGN KEY (prohibition_id)
REFERENCES public.prohibition(id);
ALTER INDEX idx__public__agreement_caveat__user_id RENAME TO idx__public__agreement_prohibition__user_id;
ALTER INDEX idx__public__agreement_caveat__agreement_id RENAME TO idx__public__agreement_prohibition__agreement_id;
ALTER INDEX idx__public__agreement_caveat__caveat_id RENAME TO idx__public__agreement_prohibition__prohibition_id;
ALTER INDEX idx__public__agreement_caveat__created_ts RENAME TO idx__public__agreement_prohibition__created_ts;
ALTER INDEX idx__public__agreement_caveat__updated_ts RENAME TO idx__public__agreement_prohibition__updated_ts;
ALTER TABLE agreement_content ADD COLUMN key VARCHAR(255) UNIQUE;
CREATE INDEX idx__agreement_content__key ON public.agreement_content (key);
ALTER TABLE agreement_content ADD COLUMN agreement_uuid UUID UNIQUE NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
ALTER TABLE purpose ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE prohibition ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE role ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_purpose ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_prohibition ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE agreement_role ALTER COLUMN user_id DROP NOT NULL;
ALTER TABLE public.purpose DROP CONSTRAINT uniq__purpose;
CREATE UNIQUE INDEX uniq__purpose ON public.purpose (COALESCE(user_id, -1), value);
ALTER TABLE public.prohibition DROP CONSTRAINT uniq__prohibition;
CREATE UNIQUE INDEX uniq__prohibition ON public.prohibition (COALESCE(user_id, -1), value);
ALTER TABLE public.role DROP CONSTRAINT uniq__role;
CREATE UNIQUE INDEX uniq__role ON public.role (COALESCE(user_id, -1), value);
ALTER TABLE public.agreement_purpose DROP CONSTRAINT uniq__agreement_purpose;
CREATE UNIQUE INDEX uniq__agreement_purpose ON public.agreement_purpose (COALESCE(user_id, -1), agreement_id, purpose_id);
ALTER TABLE public.agreement_prohibition DROP CONSTRAINT uniq__agreement_prohibition;
CREATE UNIQUE INDEX uniq__agreement_prohibition ON public.agreement_prohibition (COALESCE(user_id, -1), agreement_id, prohibition_id);
ALTER TABLE public.agreement_role DROP CONSTRAINT uniq__agreement_role;
CREATE UNIQUE INDEX uniq__agreement_role ON public.agreement_role (COALESCE(user_id, -1), agreement_id, role_id);
ALTER TABLE public.agreement ADD COLUMN context VARCHAR(255) DEFAULT 'https://protocol.jlinc.org/context/jlinc-v7.jsonld';

View File

@@ -26,7 +26,40 @@ async function getAgreementContent(userId, hash) {
} finally {
await client.release();
}
return marked(content);
return content;
}
export async function getAgreements(userId) {
let agreements = [];
const client = await getPool();
try {
let sql = `
SELECT
title,
hash
FROM agreement_content
WHERE user_id IS null
`;
let values = [];
if (userId) {
sql += `
OR user_id = $1
`;
values.push(userId);
}
sql += `
ORDER BY title ASC
`
const res = await client.query(sql, values);
if (res.rows.length > 0) {
agreements = res.rows;
}
} catch(e) {
console.error(e)
} finally {
await client.release();
}
return agreements;
}
export function routeAgreements(app) {
@@ -34,14 +67,30 @@ export function routeAgreements(app) {
app.get('/agreements/:hash', async (req, res) => {
const { hash } = req.params;
const agreement = await getAgreementContent(null, hash);
res.send(agreement);
res.render('agreement', {
agreement: marked(agreement),
rawUrl: `/agreements/${hash}/raw`,
});
});
app.get('/agreements/:hash/raw', async (req, res) => {
const { hash } = req.params;
const agreement = await getAgreementContent(null, hash);
res.send(`<pre>${agreement}</pre>`);
});
// Route for /agreements/:userid/:hash
app.get('/agreements/:userId/:hash', async (req, res) => {
const { userId, hash } = req.params;
const agreement = await getAgreementContent(userId, hash);
res.send(agreement);
res.render('agreement', {
agreement: marked(agreement),
rawUrl: `/agreements/${hash}/raw`,
});
});
app.get('/agreements/:userId/:hash/raw', async (req, res) => {
const { hash } = req.params;
const agreement = await getAgreementContent(userId, hash);
res.send(`<pre>${agreement}</pre>`);
});
}

View File

@@ -1,9 +1,11 @@
import bodyParser from "body-parser";
import { initModules, apiMiddleware } from "./auth.js";
import { loadPep } from "../modules/pep/index.js";
import swaggerUi from "swagger-ui-express";
import swaggerDocument from "./api/v1/swagger.json" assert { type: "json" };
import swaggerDocument from "./api/v1/swagger.json" with { type: "json" };
import { getConfig } from "../common/config.js";
import { core } from "../modules/core/index.js"
import { getAgreements } from "../http/agreements.js"
import express from "express";
import session from "express-session";
@@ -34,8 +36,10 @@ async function renderPrivate(view, req, res, config) {
const begin = new Date(now.getFullYear(), now.getMonth(), 1);
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
const usage = await getUsage(req.user, begin, end);
const agreements = await getAgreements(req?.user?.id);
res.render(view, {
config,
agreements,
user: req.user,
usage,
});
@@ -80,12 +84,14 @@ export async function initHTTP(app) {
logRequest(app);
routeAgreements(app);
await loadPep(app);
app.get("/", (req, res) => render('login', res, config));
app.get("/dashboard", (req, res) => renderPrivate('dashboard', req, res, config));
app.get("/refresh", refresh);
app.post('/logout', logout);
app.post("/api/v1/*", bodyParser.json(), apiMiddleware, core.post);
app.post(/^\/api\/v1\/.*$/, bodyParser.json(), apiMiddleware, core.post);
// app.use("/api/v1", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
}

View File

@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<%- include('./include/header.ejs', { title: 'JLINC - MyTerms Agreement' }) %>
<style>
a {
color: #31A9BA !important
}
</style>
<div class="mdc-card" style="background: linear-gradient(333deg, rgb(0, 0, 0) 0%, rgb(79, 55, 139) 100%);">
<%- agreement %>
</div>
<br>
View the <a href="<%- rawUrl %>">raw agreement content</a>.
<%- include('./include/footer.ejs') %>

View File

@@ -12,4 +12,10 @@
%>
<% } %>
<%-
include('./include/agreements.ejs', {
app: config.appModules['core']
})
%>
<%- include('./include/footer.ejs') %>

View File

@@ -0,0 +1,26 @@
<% const cardStyle=`"background: linear-gradient(333deg, ${app.background.color1} 0%, ${app.background.color2} 100%);
margin-bottom: 30px;"`; const userApp=user.apps.find(ua=> app.type === 'core');
const buttonStyle = `"padding: 8px 10px 8px 10px; border-radius: 16px !important; background-color:
${app.button.color} !important; border: none !important;"`
%>
<div class="mdc-card mdc-theme--dark" style=<%- cardStyle %>>
<div style="display: flex; align-items: center;">
<div style="width: 30px">
<%- app.logo %>
</div>
<h2 style="margin-left: 10px; padding-bottom: 10px;">
Available Agreements
</h2>
</div>
<div style="display: flex; align-items: center;">
<ul style="margin-top: 0px">
<% for (const agreement of agreements) { %>
<li style="padding-bottom: 1em"><a target="_new" class="agreement-link" href="/agreements/<%- agreement.hash %>"><%- agreement.title %></a></li>
<% } %>
</ul>
</div>
</div>

View File

@@ -1,3 +1,5 @@
</div>
</center>
</div>
</body>

View File

@@ -64,6 +64,11 @@
/* @include button.ink-color(#84565E); */
}
.agreement-link {
font-family: var(--md-ref-typeface-plain);
color: white;
}
</style>
</head>
@@ -109,10 +114,12 @@
</script>
<% } %>
</div>
<div style="width: 90%; max-width: 600px;">
<div
style="display: flex; align-items: center; justify-content: center; gap: 0px; text-align: center; transform: translateX(-0px);">
<div style="width: 200px; padding-bottom: 26px">
<%- include('./logo-white.svg') %>
</div>
</div>
<div style="width: 100%; overflow-y: auto; max-height: 100%; word-wrap: break-word;">
<center>
<div style="width: 90%; max-width: 600px; text-align: left">
<div
style="padding-top: 20px; display: flex; align-items: center; justify-content: center; gap: 0px; text-align: center; transform: translateX(-0px);">
<div style="width: 200px; padding-bottom: 26px">
<%- include('./logo-white.svg') %>
</div>
</div>

View File

@@ -7,13 +7,15 @@
<h3>Login with:</h3>
<% for (const type in config.authModules) { %>
<div style="width: 100%; padding-bottom: 16px">
<button style="width: 100%" class="mdc-button mdc-button--raised mdc-button--leading" onclick="window.location.href='/login/<%= type %>'">
<span class="mdc-button__ripple"></span>
<i class="material-icons mdc-button__icon" aria-hidden="true"><%= config.authModules[type].icon %></i>
<span class="mdc-button__label"><%= config.authModules[type].title %></span>
</button>
</div>
<% if (type !== 'single') { %>
<div style="width: 100%; padding-bottom: 16px">
<button style="width: 100%" class="mdc-button mdc-button--raised mdc-button--leading" onclick="window.location.href='/login/<%= type %>'">
<span class="mdc-button__ripple"></span>
<i class="material-icons mdc-button__icon" aria-hidden="true"><%= config.authModules[type].icon %></i>
<span class="mdc-button__label"><%= config.authModules[type].title %></span>
</button>
</div>
<% } %>
<% } %>
</div>

View File

@@ -3,6 +3,10 @@ const { JlincAgreement } = pkg;
async function create(input) {
if (data.caveats) {
data.prohibitions = data.caveats;
delete data.caveats;
}
const data = await JlincAgreement.create(input);
const message = data?.agreementId;
return {

View File

@@ -4,11 +4,14 @@ import { getPool } from "../../../db/index.js";
import { entity } from "./entity.js";
import { putQueue } from "../../../common/queue.js";
async function getAgreement(client, userId, id) {
async function getAgreement(client, userId, id, key) {
const whereClause = id ? `a.agreement_id_uuid = $1` : `a.agreement_id_uuid = (SELECT agreement_uuid FROM agreement_content WHERE lower(key) = lower($1))`;
const whereValue = id ? id : key;
const sql = `
SELECT
JSON_BUILD_OBJECT(
'version', a.version,
'@context', a.context,
'parent', a.parent,
'agreementId', a.agreement_id_uuid,
'created', a.created,
@@ -20,41 +23,58 @@ async function getAgreement(client, userId, id) {
'purposes', (
SELECT JSON_AGG(p.value ORDER BY p.value)
FROM purpose p
LEFT JOIN agreement_purpose ap
INNER JOIN agreement_purpose ap
ON p.id = ap.purpose_id
AND ap.agreement_id = a.id
AND p.user_id = a.user_id
AND ap.user_id = a.user_id
AND (
(p.user_id = a.user_id AND ap.user_id = a.user_id)
OR
(p.user_id IS NULL AND ap.user_id IS NULL AND a.user_id IS NULL)
)
),
'caveats', (
'prohibitions', (
SELECT JSON_AGG(c.value ORDER BY c.value)
FROM caveat c
LEFT JOIN agreement_caveat ac
ON c.id = ac.caveat_id
FROM prohibition c
INNER JOIN agreement_prohibition ac
ON c.id = ac.prohibition_id
AND ac.agreement_id = a.id
AND c.user_id = a.user_id
AND ac.user_id = a.user_id
AND (
(c.user_id = ac.user_id AND ac.user_id = a.user_id)
OR
(c.user_id IS NULL AND ac.user_id IS NULL AND a.user_id IS NULL)
)
),
'validRoles', (
SELECT JSON_AGG(r.value ORDER BY r.value)
FROM role r
LEFT JOIN agreement_role ar
INNER JOIN agreement_role ar
ON r.id = ar.role_id
AND ar.agreement_id = a.id
AND r.user_id = a.user_id
AND ar.user_id = a.user_id
AND (
(r.user_id = ar.user_id AND ar.user_id = a.user_id)
OR
(r.user_id IS NULL AND ar.user_id IS NULL AND a.user_id IS NULL)
)
)
) AS record
FROM agreement a
WHERE a.agreement_id_uuid = $1
AND a.user_id = $2;
WHERE ${whereClause}
AND (
a.user_id = $2
OR a.user_id IS NULL
)
`
const res = await client.query(sql, [
id,
whereValue,
userId,
]);
if (res.rows.length > 0 && res.rows[0].record) {
return res.rows[0].record;
const ret = res.rows[0].record;
if (!ret.ids) ret.ids = [];
if (!ret.purposes) ret.purposes = [];
if (!ret.prohibitions) ret.prohibitions = [];
if (!ret.validRoles) ret.validRoles = [];
return ret;
}
return null;
}
@@ -99,7 +119,7 @@ async function get(input, userId) {
};
const client = await getPool();
try {
const existingAgreement = await getAgreement(client, userId, input.agreementId)
const existingAgreement = await getAgreement(client, userId, input.agreementId, input.key)
if (!existingAgreement) {
response.error = 'agreement not found'
} else {
@@ -135,6 +155,7 @@ async function save(client, userId, agreement) {
version,
parent,
agreement_id_uuid,
context,
created,
created_as_ts
) VALUES (
@@ -143,13 +164,15 @@ async function save(client, userId, agreement) {
$3,
$4,
$5,
$6
$6,
$7
) RETURNING id;
`, [
userId,
agreement.version,
agreement.parent,
agreement.agreementId,
agreement["@context"],
agreement.created,
new Date(agreement.created).toISOString(),
]);
@@ -195,7 +218,7 @@ async function save(client, userId, agreement) {
SELECT id
FROM purpose
WHERE value = $3
AND user_id = $1
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
@@ -204,9 +227,9 @@ async function save(client, userId, agreement) {
purpose,
]);
}
for (const caveat of agreement.caveats) {
for (const prohibition of agreement.prohibitions) {
await client.query(`
INSERT INTO caveat (
INSERT INTO prohibition (
user_id,
value
) VALUES (
@@ -215,27 +238,27 @@ async function save(client, userId, agreement) {
) ON CONFLICT DO NOTHING;
`, [
userId,
caveat,
prohibition,
]);
await client.query(`
INSERT INTO agreement_caveat (
INSERT INTO agreement_prohibition (
user_id,
agreement_id,
caveat_id
prohibition_id
) VALUES (
$1,
$2,
(
SELECT id
FROM caveat
FROM prohibition
WHERE value = $3
AND user_id = $1
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
userId,
res.rows[0].id,
caveat,
prohibition,
]);
}
for (const role of agreement.validRoles) {
@@ -263,7 +286,7 @@ async function save(client, userId, agreement) {
SELECT id
FROM role
WHERE value = $3
AND user_id = $1
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
@@ -340,6 +363,10 @@ async function create(input, userId, _client, uuid) {
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
}
delete input.shortNames;
if (input.caveats) {
input.prohibitions = input.caveats;
delete input.caveats;
}
const agreement = await JlincAgreement.create(input);
if (uuid) {
agreement.agreementId = uuid;

View File

@@ -6,156 +6,186 @@ import { data } from "./data/index.js";
import { archive } from "./archive.js";
import { trackUsage } from "./usage.js";
import { getConfig } from "../../common/config.js";
import { evaluate } from "../pep/index.js";
async function post(req, res) {
let response = {
success: false,
error: 'Unknown error',
};
let errorCode = 400;
let type;
const prefix = `${req.method} ${req.url}`;
let evaluation = false;
try {
const input = req.body;
if (input.auth) {
evaluation = await evaluate(input.auth);
} else {
evaluation = true;
}
const config = getConfig();
if (Object.keys(config.appModules).includes('core')) {
switch (req.url) {
case '/api/v1/auth':
evaluation = await evaluate(input);
if (evaluation)
response = {
data: {
decision: true
},
message: 'Authorization allowed'
};
type = 'core';
break;
case '/api/v1/did/create':
response = await did.create(input);
if (evaluation) response = await did.create(input);
type = 'core';
break;
case '/api/v1/did/rotate':
response = await did.rotate(input);
if (evaluation) response = await did.rotate(input);
type = 'core';
break;
case '/api/v1/did/updateServices':
response = await did.updateServices(input);
if (evaluation) response = await did.updateServices(input);
type = 'core';
break;
case '/api/v1/did/send':
response = await did.send(input);
if (evaluation) response = await did.send(input);
type = 'core';
break;
case '/api/v1/did/resolve':
response = await did.resolve(input);
if (evaluation) response = await did.resolve(input);
type = 'core';
break;
case '/api/v1/agreement/create':
response = await agreement.create(input);
if (evaluation) response = await agreement.create(input);
type = 'core';
break;
case '/api/v1/agreement/sign':
response = await agreement.sign(input);
if (evaluation) response = await agreement.sign(input);
type = 'core';
break;
case '/api/v1/agreement/send':
response = await agreement.send(input);
if (evaluation) response = await agreement.send(input);
type = 'core';
break;
case '/api/v1/event/create':
response = await event.create(input);
if (evaluation) response = await event.create(input);
type = 'core';
break;
case '/api/v1/event/sign':
response = await event.sign(input);
if (evaluation) response = await event.sign(input);
type = 'core';
break;
case '/api/v1/event/send':
response = await event.send(input);
if (evaluation) response = await event.send(input);
type = 'core';
break;
case '/api/v1/audit/create':
response = await audit.create(input);
if (evaluation) response = await audit.create(input);
type = 'core';
break;
case '/api/v1/audit/sign':
response = await audit.sign(input);
if (evaluation) response = await audit.sign(input);
type = 'core';
break;
case '/api/v1/audit/send':
response = await audit.send(input);
if (evaluation) response = await audit.send(input);
type = 'core';
break;
case '/api/v1/data/entity/get':
response = await data.entity.get(input, req.session.user_id);
if (evaluation) response = await data.entity.get(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/entity/domains/get':
response = await data.entity.getDomains(input, req.session.user_id);
if (evaluation) response = await data.entity.getDomains(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/entity/create':
response = await data.entity.create(input, req.session.user_id);
if (evaluation) response = await data.entity.create(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/agreement/get':
response = await data.agreement.get(input, req.session.user_id);
if (evaluation) response = await data.agreement.get(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/agreement/create':
response = await data.agreement.create(input, req.session.user_id);
if (evaluation) response = await data.agreement.create(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/agreement/process':
response = await data.agreement.process(input, req.session.user_id);
if (evaluation) response = await data.agreement.process(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/agreement/produce':
response = await data.agreement.produce(input, req.session.user_id);
if (evaluation) response = await data.agreement.produce(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/event/get':
response = await data.event.get(input, req.session.user_id);
if (evaluation) response = await data.event.get(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/event/create':
response = await data.event.create(input, req.session.user_id);
if (evaluation) response = await data.event.create(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/event/process':
response = await data.event.process(input, req.session.user_id);
if (evaluation) response = await data.event.process(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/event/produce':
response = await data.event.produce(input, req.session.user_id);
if (evaluation) response = await data.event.produce(input, req.session.user_id);
type = 'core';
break;
case '/api/v1/data/audit/verify':
response = await data.audit.verify(input, req.session.user_id);
if (evaluation) response = await data.audit.verify(input, req.session.user_id);
type = 'core';
break;
}
}
if (Object.keys(config.appModules).includes('archive')) {
switch (req.url) {
case '/api/v1/audit/put':
response = await archive.put(input);
type = 'archive';
break;
case '/api/v1/audit/get':
response = await archive.get(input);
type = 'archive';
break;
}
}
if (!type) {
response.error = 'Page not found';
res.status(404).send(response);
if (Object.keys(config.appModules).includes('archive')) {
switch (req.url) {
case '/api/v1/audit/put':
if (evaluation) response = await archive.put(input);
type = 'archive';
break;
case '/api/v1/audit/get':
if (evaluation) response = await archive.get(input);
type = 'archive';
break;
}
}
if (!type) {
response.error = 'Page not found';
errorCode = 404;
}
}
} catch (e) {
console.error(e);
response.error = e.message;
} finally {
if (response?.message) {
req.apiMessage = response.message;
} else if (response?.data?.error) {
req.apiMessage = `ERROR: ${response.data.error}`;
if (evaluation) {
if (response?.message) {
req.apiMessage = response.message;
} else if (response?.data?.error) {
req.apiMessage = `ERROR: ${response.data.error}`;
} else {
req.apiMessage = `ERROR: unknown error`;
}
if (response?.data)
response = response.data;
res.status(response?.error ? errorCode : 200).json(response);
} else {
req.apiMessage = `ERROR: unknown error`;
response = {
data: {
decision: false
},
message: 'Authorization denied'
}
req.apiMessage = response.message;
res.status(200).json(response.data);
}
if (response?.data)
response = response.data;
await trackUsage(req.session.user_id, req.url, type, response?.error ? false : true);
res.status(response?.error ? 400 : 200).json(response);
}
}

63
src/modules/pep/cerbos.js Normal file
View File

@@ -0,0 +1,63 @@
import { getConfig } from "../../common/config.js";
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
export async function init() {
const config = getConfig();
// No initialization required
}
export async function check(req) {
// Ref:
// ----
// curl -X POST http://localhost:3592/api/check/resources \
// -H "Content-Type: application/json" \
// -d '{
// "requestId": "test-check-1",
// "principal": {
// "id": "user123",
// "roles": ["user"]
// },
// "resources": [
// {
// "resource": {
// "kind": "privateData",
// "id": "record001",
// "attr": {}
// },
// "actions": ["read"]
// }
// ]
// }'
const r = {
requestId: uuidv4(),
principal: {
id: req.subject.id,
roles: [req.subject.type]
},
resources: [
{
resource: {
kind: req.resource.type,
id: req.resource.id,
attr: {}
},
actions: [req.action.name]
}
]
}
if (req.resource.properties.ownerID) {
r.resources[0].resource.attr = { owner: req.resource.properties.ownerID }
}
const config = getConfig();
const result = await axios.post(
`${config.pdpUrl}/api/check/resources`,
r,
)
console.log(`Auth check: ${JSON.stringify(result.data)}`);
if (result.data?.results[0]?.actions[req.action.name] === 'EFFECT_ALLOW') {
return true;
}
return false;
}

35
src/modules/pep/index.js Normal file
View File

@@ -0,0 +1,35 @@
import { getConfig } from "../../common/config.js";
let evaluator;
export async function loadPep(app) {
const config = getConfig();
if (config.pdpType) {
const pepModulePath = `./${config.pdpType}.js`;
const { init, check } = await import(pepModulePath);
await init();
// // Follow AuthZEN base format
// // {
// // subject: {
// // type: "user",
// // id: "1abc",
// // },
// // action: {
// // name: "read"
// // },
// // resource: {
// // type: "data",
// // id: "2def",
// // properties: {
// // ownerID: "my@me.com"
// // }
// // }
// // }
evaluator = check;
}
}
export async function evaluate(req) {
return await evaluator(req);
}

9663
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,31 +20,32 @@
"license": "SSPLv1",
"dependencies": {
"@jlinc/core": "file:./packages/core",
"axios": "^1.10.0",
"body-parser": "^1.20.3",
"axios": "^1.13.1",
"body-parser": "^2.2.0",
"ejs": "^3.1.10",
"express": "^4.21.2",
"express-session": "^1.18.1",
"marked": "^16.1.1",
"express": "^5.1.0",
"express-session": "^1.18.2",
"marked": "^16.4.1",
"memorystore": "^1.6.7",
"passport": "^0.7.0",
"passport-github": "^1.1.0",
"passport-google-oauth20": "^2.0.0",
"passport-openidconnect": "^0.1.2",
"pg": "^8.13.1",
"pg": "^8.16.3",
"safe-stable-stringify": "^2.5.0",
"sodium-native": "^5.0.6",
"swagger-ui-express": "^5.0.1"
"sodium-native": "^5.0.9",
"swagger-ui-express": "^5.0.1",
"uuid": "^13.0.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-syntax-import-assertions": "^7.26.0",
"@eslint/js": "^9.17.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"globals": "^15.14.0",
"jest": "^29.7.0",
"nodemon": "^3.1.9",
"prettier": "^3.4.2"
"@babel/eslint-parser": "^7.28.5",
"@babel/plugin-syntax-import-assertions": "^7.27.1",
"@eslint/js": "^9.39.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"globals": "^16.5.0",
"jest": "^30.2.0",
"nodemon": "^3.1.10",
"prettier": "^3.6.2"
}
}