add auth functionality
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20
|
FROM node:25
|
||||||
ADD src/package*.json /app/
|
ADD src/package*.json /app/
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ services:
|
|||||||
# - Set up auth: https://console.cloud.google.com/auth/overview
|
# - Set up auth: https://console.cloud.google.com/auth/overview
|
||||||
# GOOGLE_CLIENT_ID:
|
# GOOGLE_CLIENT_ID:
|
||||||
# GOOGLE_CLIENT_SECRET:
|
# GOOGLE_CLIENT_SECRET:
|
||||||
|
|
||||||
|
# PDP MODULES
|
||||||
|
# ===========
|
||||||
|
PDP_TYPE: cerbos
|
||||||
|
|
||||||
|
# Cerbos
|
||||||
|
# -----------
|
||||||
|
PDP_URL: http://jlinc-pdp:3592
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- jlinc-db
|
- jlinc-db
|
||||||
@@ -83,5 +91,16 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- jlinc
|
- 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:
|
networks:
|
||||||
jlinc:
|
jlinc:
|
||||||
|
|||||||
41
policies/data.yaml
Normal file
41
policies/data.yaml
Normal 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
41
policies/data_test.yaml
Normal 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
40
policies/privateData.yaml
Normal 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
|
||||||
40
policies/privateData_test.yaml
Normal file
40
policies/privateData_test.yaml
Normal 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
19
policies/testdata/principals.yaml
vendored
Normal 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
12
policies/testdata/resources.yaml
vendored
Normal 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: {}
|
||||||
@@ -15,9 +15,9 @@ export async function loadConfig() {
|
|||||||
const authModules = process.env.AUTH_MODULES.split(',').map(a => a.trim());
|
const authModules = process.env.AUTH_MODULES.split(',').map(a => a.trim());
|
||||||
for (const type of authModules) {
|
for (const type of authModules) {
|
||||||
const authModulePath = `../modules/auth/${type}.js`;
|
const authModulePath = `../modules/auth/${type}.js`;
|
||||||
const { getModuleConfig } = await import(authModulePath);
|
const { getModuleConfig } = await import(authModulePath);
|
||||||
config.authModules[type] = getModuleConfig();
|
config.authModules[type] = getModuleConfig();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.appModules = {};
|
config.appModules = {};
|
||||||
let appModules = [
|
let appModules = [
|
||||||
@@ -29,9 +29,11 @@ export async function loadConfig() {
|
|||||||
}
|
}
|
||||||
for (const type of appModules) {
|
for (const type of appModules) {
|
||||||
const appModulePath = `../modules/app/${type}.js`;
|
const appModulePath = `../modules/app/${type}.js`;
|
||||||
const { getModuleConfig } = await import(appModulePath);
|
const { getModuleConfig } = await import(appModulePath);
|
||||||
config.appModules[type] = getModuleConfig();
|
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() {
|
export function getConfig() {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"purposes": [],
|
||||||
|
"prohibitions": []
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ export async function migrate() {
|
|||||||
await client.query(`
|
await client.query(`
|
||||||
DROP SCHEMA IF EXISTS system CASCADE;
|
DROP SCHEMA IF EXISTS system CASCADE;
|
||||||
CREATE SCHEMA system;
|
CREATE SCHEMA system;
|
||||||
|
|
||||||
-- Migrations
|
-- Migrations
|
||||||
DROP TABLE IF EXISTS system.migrate CASCADE;
|
DROP TABLE IF EXISTS system.migrate CASCADE;
|
||||||
CREATE TABLE system.migrate (
|
CREATE TABLE system.migrate (
|
||||||
@@ -131,9 +131,12 @@ export async function populateAgreements() {
|
|||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
});
|
});
|
||||||
for await (const file of files) {
|
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 title = file.name.slice(39, file.name.length - 3);
|
||||||
const markdown = fs.readFileSync(path.join("./db/agreements", file.name), "utf8").trim();
|
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')
|
const hash = createHash('sha256')
|
||||||
.update(markdown)
|
.update(markdown)
|
||||||
.digest('hex')
|
.digest('hex')
|
||||||
@@ -141,40 +144,48 @@ export async function populateAgreements() {
|
|||||||
const agreementExists = await client.query(`
|
const agreementExists = await client.query(`
|
||||||
SELECT
|
SELECT
|
||||||
CASE
|
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
|
THEN TRUE
|
||||||
ELSE FALSE
|
ELSE FALSE
|
||||||
END AS exists
|
END AS exists
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
agreementId,
|
title,
|
||||||
|
hash,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if (!agreementExists.rows[0].exists) {
|
if (!agreementExists.rows[0].exists) {
|
||||||
console.log(`Adding agreement '${title}' (${agreementId})`);
|
console.log(`Adding agreement '${title}' (${agreementUuid})`);
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO agreement_content (
|
INSERT INTO agreement_content (
|
||||||
title,
|
title,
|
||||||
markdown,
|
markdown,
|
||||||
hash
|
hash,
|
||||||
|
key,
|
||||||
|
agreement_uuid
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
$3
|
$3,
|
||||||
) ON CONFLICT DO NOTHING;
|
$4,
|
||||||
|
$5
|
||||||
|
);
|
||||||
`, [
|
`, [
|
||||||
title,
|
title,
|
||||||
markdown,
|
markdown,
|
||||||
hash,
|
hash,
|
||||||
|
json.key,
|
||||||
|
agreementUuid,
|
||||||
]);
|
]);
|
||||||
const agreement = {
|
const agreement = {
|
||||||
|
"@context": json["@context"] || 'https://protocol.jlinc.org/context/jlinc-v7.jsonld',
|
||||||
uri: `${config.publicCoreUrl}/agreements/${hash}`,
|
uri: `${config.publicCoreUrl}/agreements/${hash}`,
|
||||||
purposes: [],
|
purposes: json.purposes || [],
|
||||||
caveats: [],
|
prohibitions: json.prohibitions || [],
|
||||||
shortNames: [],
|
shortNames: [],
|
||||||
validRoles: [],
|
validRoles: json.validRoles || [],
|
||||||
}
|
}
|
||||||
await data.agreement.create(agreement, null, client, agreementId);
|
await data.agreement.create(agreement, null, client, agreementUuid);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
46
src/db/migrations/000010 - prohibition.sql
Normal file
46
src/db/migrations/000010 - prohibition.sql
Normal 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';
|
||||||
@@ -25,8 +25,41 @@ async function getAgreementContent(userId, hash) {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
await client.release();
|
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) {
|
export function routeAgreements(app) {
|
||||||
@@ -34,14 +67,30 @@ export function routeAgreements(app) {
|
|||||||
app.get('/agreements/:hash', async (req, res) => {
|
app.get('/agreements/:hash', async (req, res) => {
|
||||||
const { hash } = req.params;
|
const { hash } = req.params;
|
||||||
const agreement = await getAgreementContent(null, hash);
|
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) => {
|
app.get('/agreements/:userId/:hash', async (req, res) => {
|
||||||
const { userId, hash } = req.params;
|
const { userId, hash } = req.params;
|
||||||
const agreement = await getAgreementContent(userId, hash);
|
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>`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import bodyParser from "body-parser";
|
import bodyParser from "body-parser";
|
||||||
import { initModules, apiMiddleware } from "./auth.js";
|
import { initModules, apiMiddleware } from "./auth.js";
|
||||||
|
import { loadPep } from "../modules/pep/index.js";
|
||||||
import swaggerUi from "swagger-ui-express";
|
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 { getConfig } from "../common/config.js";
|
||||||
import { core } from "../modules/core/index.js"
|
import { core } from "../modules/core/index.js"
|
||||||
|
import { getAgreements } from "../http/agreements.js"
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import session from "express-session";
|
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 begin = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
const usage = await getUsage(req.user, begin, end);
|
const usage = await getUsage(req.user, begin, end);
|
||||||
|
const agreements = await getAgreements(req?.user?.id);
|
||||||
res.render(view, {
|
res.render(view, {
|
||||||
config,
|
config,
|
||||||
|
agreements,
|
||||||
user: req.user,
|
user: req.user,
|
||||||
usage,
|
usage,
|
||||||
});
|
});
|
||||||
@@ -80,12 +84,14 @@ export async function initHTTP(app) {
|
|||||||
logRequest(app);
|
logRequest(app);
|
||||||
routeAgreements(app);
|
routeAgreements(app);
|
||||||
|
|
||||||
|
await loadPep(app);
|
||||||
|
|
||||||
app.get("/", (req, res) => render('login', res, config));
|
app.get("/", (req, res) => render('login', res, config));
|
||||||
app.get("/dashboard", (req, res) => renderPrivate('dashboard', req, res, config));
|
app.get("/dashboard", (req, res) => renderPrivate('dashboard', req, res, config));
|
||||||
app.get("/refresh", refresh);
|
app.get("/refresh", refresh);
|
||||||
app.post('/logout', logout);
|
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));
|
// app.use("/api/v1", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function logout(req, res, next) {
|
|||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const strategyConfig = config.authModules[strategy];
|
const strategyConfig = config.authModules[strategy];
|
||||||
if (strategyConfig && strategyConfig.logoutURL) {
|
if (strategyConfig && strategyConfig.logoutURL) {
|
||||||
res.redirect(strategyConfig.logoutURL);
|
res.redirect(strategyConfig.logoutURL);
|
||||||
} else {
|
} else {
|
||||||
res.redirect('/');
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/http/views/agreement.ejs
Normal file
21
src/http/views/agreement.ejs
Normal 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') %>
|
||||||
@@ -12,4 +12,10 @@
|
|||||||
%>
|
%>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<%-
|
||||||
|
include('./include/agreements.ejs', {
|
||||||
|
app: config.appModules['core']
|
||||||
|
})
|
||||||
|
%>
|
||||||
|
|
||||||
<%- include('./include/footer.ejs') %>
|
<%- include('./include/footer.ejs') %>
|
||||||
|
|||||||
26
src/http/views/include/agreements.ejs
Normal file
26
src/http/views/include/agreements.ejs
Normal 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>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
</div>
|
||||||
|
</center>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,11 @@
|
|||||||
|
|
||||||
/* @include button.ink-color(#84565E); */
|
/* @include button.ink-color(#84565E); */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agreement-link {
|
||||||
|
font-family: var(--md-ref-typeface-plain);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -109,10 +114,12 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 90%; max-width: 600px;">
|
<div style="width: 100%; overflow-y: auto; max-height: 100%; word-wrap: break-word;">
|
||||||
<div
|
<center>
|
||||||
style="display: flex; align-items: center; justify-content: center; gap: 0px; text-align: center; transform: translateX(-0px);">
|
<div style="width: 90%; max-width: 600px; text-align: left">
|
||||||
<div style="width: 200px; padding-bottom: 26px">
|
<div
|
||||||
<%- include('./logo-white.svg') %>
|
style="padding-top: 20px; display: flex; align-items: center; justify-content: center; gap: 0px; text-align: center; transform: translateX(-0px);">
|
||||||
</div>
|
<div style="width: 200px; padding-bottom: 26px">
|
||||||
</div>
|
<%- include('./logo-white.svg') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -7,13 +7,15 @@
|
|||||||
|
|
||||||
<h3>Login with:</h3>
|
<h3>Login with:</h3>
|
||||||
<% for (const type in config.authModules) { %>
|
<% for (const type in config.authModules) { %>
|
||||||
<div style="width: 100%; padding-bottom: 16px">
|
<% if (type !== 'single') { %>
|
||||||
<button style="width: 100%" class="mdc-button mdc-button--raised mdc-button--leading" onclick="window.location.href='/login/<%= type %>'">
|
<div style="width: 100%; padding-bottom: 16px">
|
||||||
<span class="mdc-button__ripple"></span>
|
<button style="width: 100%" class="mdc-button mdc-button--raised mdc-button--leading" onclick="window.location.href='/login/<%= type %>'">
|
||||||
<i class="material-icons mdc-button__icon" aria-hidden="true"><%= config.authModules[type].icon %></i>
|
<span class="mdc-button__ripple"></span>
|
||||||
<span class="mdc-button__label"><%= config.authModules[type].title %></span>
|
<i class="material-icons mdc-button__icon" aria-hidden="true"><%= config.authModules[type].icon %></i>
|
||||||
</button>
|
<span class="mdc-button__label"><%= config.authModules[type].title %></span>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,5 +42,5 @@ export async function initModule(app, passport) {
|
|||||||
function (req, res) {
|
function (req, res) {
|
||||||
res.redirect('/dashboard');
|
res.redirect('/dashboard');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -48,5 +48,5 @@ export async function initModule(app, passport) {
|
|||||||
req.session.authStrategy = 'oidc';
|
req.session.authStrategy = 'oidc';
|
||||||
res.redirect('/dashboard');
|
res.redirect('/dashboard');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,10 @@ const { JlincAgreement } = pkg;
|
|||||||
|
|
||||||
|
|
||||||
async function create(input) {
|
async function create(input) {
|
||||||
|
if (data.caveats) {
|
||||||
|
data.prohibitions = data.caveats;
|
||||||
|
delete data.caveats;
|
||||||
|
}
|
||||||
const data = await JlincAgreement.create(input);
|
const data = await JlincAgreement.create(input);
|
||||||
const message = data?.agreementId;
|
const message = data?.agreementId;
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function getAuditErrors(audit) {
|
|||||||
|
|
||||||
if (audit.hashType != 'SHA256') {
|
if (audit.hashType != 'SHA256') {
|
||||||
return 'audit hash type invalid';
|
return 'audit hash type invalid';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audit.digest) {
|
if (!audit.digest) {
|
||||||
return 'audit digest should exist';
|
return 'audit digest should exist';
|
||||||
@@ -130,7 +130,7 @@ async function processAudit(data) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ async function create(input) {
|
|||||||
const data = await JlincAudit.create(input);
|
const data = await JlincAudit.create(input);
|
||||||
const message = data?.eventId
|
const message = data?.eventId
|
||||||
? data?.eventId
|
? data?.eventId
|
||||||
: data?.agreementId;
|
: data?.agreementId;
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { getPool } from "../../../db/index.js";
|
|||||||
import { entity } from "./entity.js";
|
import { entity } from "./entity.js";
|
||||||
import { putQueue } from "../../../common/queue.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 = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
JSON_BUILD_OBJECT(
|
JSON_BUILD_OBJECT(
|
||||||
'version', a.version,
|
'version', a.version,
|
||||||
|
'@context', a.context,
|
||||||
'parent', a.parent,
|
'parent', a.parent,
|
||||||
'agreementId', a.agreement_id_uuid,
|
'agreementId', a.agreement_id_uuid,
|
||||||
'created', a.created,
|
'created', a.created,
|
||||||
@@ -20,41 +23,58 @@ async function getAgreement(client, userId, id) {
|
|||||||
'purposes', (
|
'purposes', (
|
||||||
SELECT JSON_AGG(p.value ORDER BY p.value)
|
SELECT JSON_AGG(p.value ORDER BY p.value)
|
||||||
FROM purpose p
|
FROM purpose p
|
||||||
LEFT JOIN agreement_purpose ap
|
INNER JOIN agreement_purpose ap
|
||||||
ON p.id = ap.purpose_id
|
ON p.id = ap.purpose_id
|
||||||
AND ap.agreement_id = a.id
|
AND ap.agreement_id = a.id
|
||||||
AND p.user_id = a.user_id
|
AND (
|
||||||
AND ap.user_id = a.user_id
|
(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)
|
SELECT JSON_AGG(c.value ORDER BY c.value)
|
||||||
FROM caveat c
|
FROM prohibition c
|
||||||
LEFT JOIN agreement_caveat ac
|
INNER JOIN agreement_prohibition ac
|
||||||
ON c.id = ac.caveat_id
|
ON c.id = ac.prohibition_id
|
||||||
AND ac.agreement_id = a.id
|
AND ac.agreement_id = a.id
|
||||||
AND c.user_id = a.user_id
|
AND (
|
||||||
AND ac.user_id = a.user_id
|
(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', (
|
'validRoles', (
|
||||||
SELECT JSON_AGG(r.value ORDER BY r.value)
|
SELECT JSON_AGG(r.value ORDER BY r.value)
|
||||||
FROM role r
|
FROM role r
|
||||||
LEFT JOIN agreement_role ar
|
INNER JOIN agreement_role ar
|
||||||
ON r.id = ar.role_id
|
ON r.id = ar.role_id
|
||||||
AND ar.agreement_id = a.id
|
AND ar.agreement_id = a.id
|
||||||
AND r.user_id = a.user_id
|
AND (
|
||||||
AND ar.user_id = a.user_id
|
(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
|
) AS record
|
||||||
FROM agreement a
|
FROM agreement a
|
||||||
WHERE a.agreement_id_uuid = $1
|
WHERE ${whereClause}
|
||||||
AND a.user_id = $2;
|
AND (
|
||||||
|
a.user_id = $2
|
||||||
|
OR a.user_id IS NULL
|
||||||
|
)
|
||||||
`
|
`
|
||||||
const res = await client.query(sql, [
|
const res = await client.query(sql, [
|
||||||
id,
|
whereValue,
|
||||||
userId,
|
userId,
|
||||||
]);
|
]);
|
||||||
if (res.rows.length > 0 && res.rows[0].record) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -99,7 +119,7 @@ async function get(input, userId) {
|
|||||||
};
|
};
|
||||||
const client = await getPool();
|
const client = await getPool();
|
||||||
try {
|
try {
|
||||||
const existingAgreement = await getAgreement(client, userId, input.agreementId)
|
const existingAgreement = await getAgreement(client, userId, input.agreementId, input.key)
|
||||||
if (!existingAgreement) {
|
if (!existingAgreement) {
|
||||||
response.error = 'agreement not found'
|
response.error = 'agreement not found'
|
||||||
} else {
|
} else {
|
||||||
@@ -135,6 +155,7 @@ async function save(client, userId, agreement) {
|
|||||||
version,
|
version,
|
||||||
parent,
|
parent,
|
||||||
agreement_id_uuid,
|
agreement_id_uuid,
|
||||||
|
context,
|
||||||
created,
|
created,
|
||||||
created_as_ts
|
created_as_ts
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@@ -143,13 +164,15 @@ async function save(client, userId, agreement) {
|
|||||||
$3,
|
$3,
|
||||||
$4,
|
$4,
|
||||||
$5,
|
$5,
|
||||||
$6
|
$6,
|
||||||
|
$7
|
||||||
) RETURNING id;
|
) RETURNING id;
|
||||||
`, [
|
`, [
|
||||||
userId,
|
userId,
|
||||||
agreement.version,
|
agreement.version,
|
||||||
agreement.parent,
|
agreement.parent,
|
||||||
agreement.agreementId,
|
agreement.agreementId,
|
||||||
|
agreement["@context"],
|
||||||
agreement.created,
|
agreement.created,
|
||||||
new Date(agreement.created).toISOString(),
|
new Date(agreement.created).toISOString(),
|
||||||
]);
|
]);
|
||||||
@@ -195,7 +218,7 @@ async function save(client, userId, agreement) {
|
|||||||
SELECT id
|
SELECT id
|
||||||
FROM purpose
|
FROM purpose
|
||||||
WHERE value = $3
|
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,
|
purpose,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
for (const caveat of agreement.caveats) {
|
for (const prohibition of agreement.prohibitions) {
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO caveat (
|
INSERT INTO prohibition (
|
||||||
user_id,
|
user_id,
|
||||||
value
|
value
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@@ -215,27 +238,27 @@ async function save(client, userId, agreement) {
|
|||||||
) ON CONFLICT DO NOTHING;
|
) ON CONFLICT DO NOTHING;
|
||||||
`, [
|
`, [
|
||||||
userId,
|
userId,
|
||||||
caveat,
|
prohibition,
|
||||||
]);
|
]);
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO agreement_caveat (
|
INSERT INTO agreement_prohibition (
|
||||||
user_id,
|
user_id,
|
||||||
agreement_id,
|
agreement_id,
|
||||||
caveat_id
|
prohibition_id
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,
|
$1,
|
||||||
$2,
|
$2,
|
||||||
(
|
(
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM caveat
|
FROM prohibition
|
||||||
WHERE value = $3
|
WHERE value = $3
|
||||||
AND user_id = $1
|
AND user_id ${userId ? `= $1` : `IS NULL`}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
`, [
|
`, [
|
||||||
userId,
|
userId,
|
||||||
res.rows[0].id,
|
res.rows[0].id,
|
||||||
caveat,
|
prohibition,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
for (const role of agreement.validRoles) {
|
for (const role of agreement.validRoles) {
|
||||||
@@ -263,7 +286,7 @@ async function save(client, userId, agreement) {
|
|||||||
SELECT id
|
SELECT id
|
||||||
FROM role
|
FROM role
|
||||||
WHERE value = $3
|
WHERE value = $3
|
||||||
AND user_id = $1
|
AND user_id ${userId ? `= $1` : `IS NULL`}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
`, [
|
`, [
|
||||||
@@ -272,7 +295,7 @@ async function save(client, userId, agreement) {
|
|||||||
role,
|
role,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSignatures(client, userId, agreementId, signatures) {
|
async function saveSignatures(client, userId, agreementId, signatures) {
|
||||||
@@ -325,7 +348,7 @@ async function saveSignatures(client, userId, agreementId, signatures) {
|
|||||||
new Date(signature.signedOn).toISOString(),
|
new Date(signature.signedOn).toISOString(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(input, userId, _client, uuid) {
|
async function create(input, userId, _client, uuid) {
|
||||||
@@ -340,6 +363,10 @@ async function create(input, userId, _client, uuid) {
|
|||||||
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
|
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
|
||||||
}
|
}
|
||||||
delete input.shortNames;
|
delete input.shortNames;
|
||||||
|
if (input.caveats) {
|
||||||
|
input.prohibitions = input.caveats;
|
||||||
|
delete input.caveats;
|
||||||
|
}
|
||||||
const agreement = await JlincAgreement.create(input);
|
const agreement = await JlincAgreement.create(input);
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
agreement.agreementId = uuid;
|
agreement.agreementId = uuid;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async function verify(input, userId) {
|
|||||||
delete input.shortNames;
|
delete input.shortNames;
|
||||||
const organizedById = [];
|
const organizedById = [];
|
||||||
for (const item of input.audits) {
|
for (const item of input.audits) {
|
||||||
const found = organizedById.find((obi) =>
|
const found = organizedById.find((obi) =>
|
||||||
item.audit.agreementId && obi.agreementId === item.audit.agreementId ||
|
item.audit.agreementId && obi.agreementId === item.audit.agreementId ||
|
||||||
item.audit.eventId && obi.eventId === item.audit.eventId
|
item.audit.eventId && obi.eventId === item.audit.eventId
|
||||||
);
|
);
|
||||||
@@ -147,7 +147,7 @@ async function verify(input, userId) {
|
|||||||
if (
|
if (
|
||||||
(item.agreementId !== null && auditRecord.audit.agreementId === item.agreementId) ||
|
(item.agreementId !== null && auditRecord.audit.agreementId === item.agreementId) ||
|
||||||
(item.eventId !== null && auditRecord.audit.eventId === item.eventId)
|
(item.eventId !== null && auditRecord.audit.eventId === item.eventId)
|
||||||
)
|
)
|
||||||
res.results.validId = true;
|
res.results.validId = true;
|
||||||
// Does the audit hash match?
|
// Does the audit hash match?
|
||||||
// The digest was created from whichever signatures this audit record has
|
// The digest was created from whichever signatures this audit record has
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ async function saveSignatures(client, userId, eventId, signatures) {
|
|||||||
new Date(signature.signedOn).toISOString(),
|
new Date(signature.signedOn).toISOString(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create(input, userId, _client, _sender) {
|
async function create(input, userId, _client, _sender) {
|
||||||
|
|||||||
@@ -6,156 +6,186 @@ import { data } from "./data/index.js";
|
|||||||
import { archive } from "./archive.js";
|
import { archive } from "./archive.js";
|
||||||
import { trackUsage } from "./usage.js";
|
import { trackUsage } from "./usage.js";
|
||||||
import { getConfig } from "../../common/config.js";
|
import { getConfig } from "../../common/config.js";
|
||||||
|
import { evaluate } from "../pep/index.js";
|
||||||
|
|
||||||
async function post(req, res) {
|
async function post(req, res) {
|
||||||
let response = {
|
let response = {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Unknown error',
|
error: 'Unknown error',
|
||||||
};
|
};
|
||||||
|
let errorCode = 400;
|
||||||
let type;
|
let type;
|
||||||
const prefix = `${req.method} ${req.url}`;
|
const prefix = `${req.method} ${req.url}`;
|
||||||
|
let evaluation = false;
|
||||||
try {
|
try {
|
||||||
const input = req.body;
|
const input = req.body;
|
||||||
|
if (input.auth) {
|
||||||
|
evaluation = await evaluate(input.auth);
|
||||||
|
} else {
|
||||||
|
evaluation = true;
|
||||||
|
}
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
if (Object.keys(config.appModules).includes('core')) {
|
if (Object.keys(config.appModules).includes('core')) {
|
||||||
switch (req.url) {
|
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':
|
case '/api/v1/did/create':
|
||||||
response = await did.create(input);
|
if (evaluation) response = await did.create(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/did/rotate':
|
case '/api/v1/did/rotate':
|
||||||
response = await did.rotate(input);
|
if (evaluation) response = await did.rotate(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/did/updateServices':
|
case '/api/v1/did/updateServices':
|
||||||
response = await did.updateServices(input);
|
if (evaluation) response = await did.updateServices(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/did/send':
|
case '/api/v1/did/send':
|
||||||
response = await did.send(input);
|
if (evaluation) response = await did.send(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/did/resolve':
|
case '/api/v1/did/resolve':
|
||||||
response = await did.resolve(input);
|
if (evaluation) response = await did.resolve(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/agreement/create':
|
case '/api/v1/agreement/create':
|
||||||
response = await agreement.create(input);
|
if (evaluation) response = await agreement.create(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/agreement/sign':
|
case '/api/v1/agreement/sign':
|
||||||
response = await agreement.sign(input);
|
if (evaluation) response = await agreement.sign(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/agreement/send':
|
case '/api/v1/agreement/send':
|
||||||
response = await agreement.send(input);
|
if (evaluation) response = await agreement.send(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/event/create':
|
case '/api/v1/event/create':
|
||||||
response = await event.create(input);
|
if (evaluation) response = await event.create(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/event/sign':
|
case '/api/v1/event/sign':
|
||||||
response = await event.sign(input);
|
if (evaluation) response = await event.sign(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/event/send':
|
case '/api/v1/event/send':
|
||||||
response = await event.send(input);
|
if (evaluation) response = await event.send(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/audit/create':
|
case '/api/v1/audit/create':
|
||||||
response = await audit.create(input);
|
if (evaluation) response = await audit.create(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/audit/sign':
|
case '/api/v1/audit/sign':
|
||||||
response = await audit.sign(input);
|
if (evaluation) response = await audit.sign(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/audit/send':
|
case '/api/v1/audit/send':
|
||||||
response = await audit.send(input);
|
if (evaluation) response = await audit.send(input);
|
||||||
type = 'core';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/entity/get':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/entity/domains/get':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/entity/create':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/agreement/get':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/agreement/create':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/agreement/process':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/agreement/produce':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/event/get':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/event/create':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/event/process':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/event/produce':
|
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';
|
type = 'core';
|
||||||
break;
|
break;
|
||||||
case '/api/v1/data/audit/verify':
|
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';
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
if (Object.keys(config.appModules).includes('archive')) {
|
||||||
if (!type) {
|
switch (req.url) {
|
||||||
response.error = 'Page not found';
|
case '/api/v1/audit/put':
|
||||||
res.status(404).send(response);
|
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) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
response.error = e.message;
|
response.error = e.message;
|
||||||
} finally {
|
} finally {
|
||||||
if (response?.message) {
|
if (evaluation) {
|
||||||
req.apiMessage = response.message;
|
if (response?.message) {
|
||||||
} else if (response?.data?.error) {
|
req.apiMessage = response.message;
|
||||||
req.apiMessage = `ERROR: ${response.data.error}`;
|
} 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 {
|
} 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);
|
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
63
src/modules/pep/cerbos.js
Normal 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
35
src/modules/pep/index.js
Normal 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
9663
src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,31 +20,32 @@
|
|||||||
"license": "SSPLv1",
|
"license": "SSPLv1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jlinc/core": "file:./packages/core",
|
"@jlinc/core": "file:./packages/core",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.13.1",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^2.2.0",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.21.2",
|
"express": "^5.1.0",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.2",
|
||||||
"marked": "^16.1.1",
|
"marked": "^16.4.1",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-github": "^1.1.0",
|
"passport-github": "^1.1.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-openidconnect": "^0.1.2",
|
"passport-openidconnect": "^0.1.2",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.16.3",
|
||||||
"safe-stable-stringify": "^2.5.0",
|
"safe-stable-stringify": "^2.5.0",
|
||||||
"sodium-native": "^5.0.6",
|
"sodium-native": "^5.0.9",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.25.9",
|
"@babel/eslint-parser": "^7.28.5",
|
||||||
"@babel/plugin-syntax-import-assertions": "^7.26.0",
|
"@babel/plugin-syntax-import-assertions": "^7.27.1",
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.39.1",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"globals": "^15.14.0",
|
"globals": "^16.5.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.10",
|
||||||
"prettier": "^3.4.2"
|
"prettier": "^3.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user