n8n-node-authorization 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Somepharm Authorization — n8n custom node
|
|
2
|
+
|
|
3
|
+
Replaces Access Edit + Authorization (Microsoft SQL) + If Authorization with one
|
|
4
|
+
node and two outputs. This is a private self-hosted node package, not a Code node
|
|
5
|
+
snippet or a package already published to npm. Plain CommonJS source is shipped
|
|
6
|
+
in `dist`; no TypeScript build is needed.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Extract the ZIP. Copy its `n8n-node-authorization-1.0.0.tgz` to the n8n server/container
|
|
11
|
+
(for example, `/tmp/n8n-node-authorization-1.0.0.tgz`). Run as the operating-system user
|
|
12
|
+
that runs n8n, using the actual n8n user directory if different:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
mkdir -p ~/.n8n/nodes
|
|
16
|
+
cd ~/.n8n/nodes
|
|
17
|
+
npm install /tmp/n8n-node-authorization-1.0.0.tgz --omit=dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Restart n8n and search for **Somepharm Authorization**. Dependencies require npm
|
|
21
|
+
registry access at installation. Community nodes must be enabled by the instance
|
|
22
|
+
administrator. This package requires Node.js 20+; also meet your n8n version's
|
|
23
|
+
own Node.js requirement.
|
|
24
|
+
|
|
25
|
+
For Docker/Kubernetes, the usual directory is `/home/node/.n8n/nodes` for the
|
|
26
|
+
`node` user. Install into persistent storage or provision the package during
|
|
27
|
+
your deployment; an installation into a pod's temporary filesystem disappears
|
|
28
|
+
when that pod is replaced. Ensure every n8n instance that loads or executes the
|
|
29
|
+
workflow (including workers) has the same package. Do not install it in task
|
|
30
|
+
runner containers. No deployment changes have been made for you.
|
|
31
|
+
|
|
32
|
+
## Configure your replacement
|
|
33
|
+
|
|
34
|
+
| Setting | Your value |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| Microsoft SQL credential | Select existing **Dev** credential |
|
|
37
|
+
| User Reference | `{{ $('Authentication').item.json.REF_USER }}` in expression mode |
|
|
38
|
+
| Company Reference | `MKT_2012_SOMEPH` |
|
|
39
|
+
| Table Name | `COMMANDES` |
|
|
40
|
+
| Table Selector | `1` |
|
|
41
|
+
| Access | Read / Consultation (1) |
|
|
42
|
+
|
|
43
|
+
The default user expression is `$json.REF_USER`. For your supplied workflow,
|
|
44
|
+
change it to the Authentication expression above or paste the example node from
|
|
45
|
+
`examples/replacement-node.json` after installing the package. Re-select **Dev**
|
|
46
|
+
if the credential reference does not resolve on a different instance. That file
|
|
47
|
+
contains a credential reference, no password. The Authentication node must have
|
|
48
|
+
executed and have valid item linking to the input. Only use `.first()` instead
|
|
49
|
+
of `.item` if all input items intentionally share one authenticated identity.
|
|
50
|
+
|
|
51
|
+
Connect the predecessor of **Access Edit** to this node. Connect output 0
|
|
52
|
+
(**Authorized**) to the old IF true branch; connect output 1 (**Denied**) to its
|
|
53
|
+
false branch. Then remove the three old nodes.
|
|
54
|
+
|
|
55
|
+
Use a trusted Authentication result for REF_USER, and configure company/table/
|
|
56
|
+
access from trusted workflow settings. The node checks authorization only; it
|
|
57
|
+
does not authenticate an arbitrary REF_USER supplied by an end user. Keep
|
|
58
|
+
**Settings > On Error = Stop Workflow** and **Always Output Data = off** so an
|
|
59
|
+
execution failure or empty branch cannot enter the protected operation.
|
|
60
|
+
|
|
61
|
+
## Behavior
|
|
62
|
+
|
|
63
|
+
- Root administrators (`ROOT_ADMIN = 'OUI'`) are authorized.
|
|
64
|
+
- Administrators of the requested company are authorized.
|
|
65
|
+
- Otherwise the user's company, table, selector and selected permission must match.
|
|
66
|
+
- One access code per check: 1 consultation, 2 validation, 4 ajout, 8 modification,
|
|
67
|
+
16 suppression. Combined masks such as 9 are rejected, not interpreted as flags.
|
|
68
|
+
- Authorized items have numeric `D_Result: 1` and `authorized: true`.
|
|
69
|
+
- Denied items have numeric `D_Result: 0` and `authorized: false`.
|
|
70
|
+
- Incoming JSON and binary data are retained. Existing D_Result/authorized fields
|
|
71
|
+
are replaced. Each item links back to its original input index.
|
|
72
|
+
- Missing/invalid parameters, SQL errors and malformed SQL responses throw errors.
|
|
73
|
+
A database outage is not an authorization denial or an authorization grant.
|
|
74
|
+
- Parameters use varchar(50) and smallint, matching the supplied SQL. Strings
|
|
75
|
+
longer than 50 characters are rejected rather than truncated. The permissions
|
|
76
|
+
database name is fixed as DROITSMK_LA in the source.
|
|
77
|
+
- Queries are parameterized. No input value is interpolated into SQL text.
|
|
78
|
+
- One execution-local connection pool; one read-only SQL query per input item.
|
|
79
|
+
The pool closes after execution. Credential timeouts, TLS, port and domain are
|
|
80
|
+
honored. The SQL account needs SELECT access to the referenced tables/view.
|
|
81
|
+
- Company admin/root behavior matches the supplied rules for valid inputs. No
|
|
82
|
+
extra active-user, expiry, or deny-override policy is introduced.
|
|
83
|
+
|
|
84
|
+
## Develop and test
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
npm ci
|
|
88
|
+
npm test
|
|
89
|
+
python3 test/permissions.py
|
|
90
|
+
npm pack
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Tests cover output routing, item pairing, binary preservation, bound parameters,
|
|
94
|
+
invalid inputs, database failures, credential mapping, and permission rules.
|
|
95
|
+
The permission matrix uses SQLite with only the database qualifiers removed;
|
|
96
|
+
it verifies relational logic, not SQL Server connectivity or its collation.
|
|
97
|
+
|
|
98
|
+
Validated locally with Node.js 24, n8n-workflow 2.16.0 and mssql 12.7.2. A live
|
|
99
|
+
n8n editor/loader and your SQL Server were not available. Verify in your Dev
|
|
100
|
+
instance with a root admin, company admin, authorized user and denied user before
|
|
101
|
+
connecting the node to a production operation.
|
|
102
|
+
|
|
103
|
+
## References
|
|
104
|
+
|
|
105
|
+
- n8n Microsoft SQL credential definition:
|
|
106
|
+
https://github.com/n8n-io/n8n/blob/master/packages/nodes-base/credentials/MicrosoftSql.credentials.ts
|
|
107
|
+
- SQL driver's parameters, pooling and configuration:
|
|
108
|
+
https://github.com/tediousjs/node-mssql
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const sql = require('mssql');
|
|
3
|
+
const { NodeOperationError } = require('n8n-workflow');
|
|
4
|
+
const { validateInput, connectionConfig, checkAuthorization } = require('./authorization');
|
|
5
|
+
|
|
6
|
+
class SomepharmAuthorization {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: 'Somepharm Authorization',
|
|
10
|
+
name: 'somepharmAuthorization',
|
|
11
|
+
icon: 'file:authorization.svg',
|
|
12
|
+
group: ['transform'],
|
|
13
|
+
version: 1,
|
|
14
|
+
description: 'Check Somepharm permissions and route items to Authorized or Denied',
|
|
15
|
+
defaults: { name: 'Somepharm Authorization', color: '#005baa' },
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main', 'main'],
|
|
18
|
+
outputNames: ['Authorized', 'Denied'],
|
|
19
|
+
credentials: [{ name: 'microsoftSql', required: true }],
|
|
20
|
+
properties: [
|
|
21
|
+
{ displayName: 'User Reference', name: 'refUser', type: 'string', required: true,
|
|
22
|
+
default: '={{ $json.REF_USER }}', description: 'Authenticated user reference. Use a trusted authentication result.' },
|
|
23
|
+
{ displayName: 'Company Reference', name: 'refSociete', type: 'string', required: true,
|
|
24
|
+
default: 'MKT_2012_SOMEPH' },
|
|
25
|
+
{ displayName: 'Table Name', name: 'tableName', type: 'string', required: true,
|
|
26
|
+
default: 'COMMANDES' },
|
|
27
|
+
{ displayName: 'Table Selector', name: 'tableSelecteur', type: 'number', required: true,
|
|
28
|
+
default: 1, typeOptions: { minValue: -32768, maxValue: 32767, numberPrecision: 0 } },
|
|
29
|
+
{ displayName: 'Access', name: 'access', type: 'options', required: true, default: 1,
|
|
30
|
+
options: [
|
|
31
|
+
{ name: 'Read / Consultation (1)', value: 1 },
|
|
32
|
+
{ name: 'Validate / Validation (2)', value: 2 },
|
|
33
|
+
{ name: 'Create / Ajout (4)', value: 4 },
|
|
34
|
+
{ name: 'Update / Modification (8)', value: 8 },
|
|
35
|
+
{ name: 'Delete / Suppression (16)', value: 16 },
|
|
36
|
+
] },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async execute() {
|
|
42
|
+
const items = this.getInputData();
|
|
43
|
+
if (items.length === 0) return [[], []];
|
|
44
|
+
const outputs = [[], []];
|
|
45
|
+
let pool;
|
|
46
|
+
let itemIndex = 0;
|
|
47
|
+
try {
|
|
48
|
+
// Validate every item before making a connection. Values/expressions are per item.
|
|
49
|
+
const values = items.map((_, index) => {
|
|
50
|
+
itemIndex = index;
|
|
51
|
+
return validateInput(Object.fromEntries(
|
|
52
|
+
['refUser', 'refSociete', 'tableName', 'tableSelecteur', 'access']
|
|
53
|
+
.map((name) => [name, this.getNodeParameter(name, index)]),
|
|
54
|
+
));
|
|
55
|
+
});
|
|
56
|
+
itemIndex = 0;
|
|
57
|
+
const credentials = await this.getCredentials('microsoftSql');
|
|
58
|
+
// Execution-local pool avoids sharing another execution's credentials or close lifecycle.
|
|
59
|
+
pool = new sql.ConnectionPool(connectionConfig(credentials));
|
|
60
|
+
pool.on('error', () => { /* Requests report their errors through the catch below. */ });
|
|
61
|
+
await pool.connect();
|
|
62
|
+
for (itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
63
|
+
const decision = await checkAuthorization(pool, sql, values[itemIndex]);
|
|
64
|
+
const output = {
|
|
65
|
+
json: { ...items[itemIndex].json, D_Result: decision, authorized: decision === 1 },
|
|
66
|
+
pairedItem: { item: itemIndex },
|
|
67
|
+
};
|
|
68
|
+
if (items[itemIndex].binary) output.binary = items[itemIndex].binary;
|
|
69
|
+
outputs[decision === 1 ? 0 : 1].push(output);
|
|
70
|
+
}
|
|
71
|
+
return outputs;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// Never convert infrastructure errors into successful authorization results.
|
|
74
|
+
throw new NodeOperationError(this.getNode(), error, { itemIndex });
|
|
75
|
+
} finally {
|
|
76
|
+
if (pool) await pool.close().catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
module.exports = { SomepharmAuthorization };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// These are fixed identifiers; all workflow-supplied values are bound parameters.
|
|
4
|
+
const AUTHORIZATION_SQL = `
|
|
5
|
+
SELECT CAST(CASE WHEN
|
|
6
|
+
EXISTS (
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM DROITSMK_LA.dbo.T_USERS U
|
|
9
|
+
LEFT JOIN DROITSMK_LA.dbo.V_USER_SOCIETES US ON U.REF_USER = US.REF_USER
|
|
10
|
+
LEFT JOIN DROITSMK_LA.dbo.T_SOCIETES S ON US.REF_SOCIETE = S.REF_SOCIETE
|
|
11
|
+
WHERE U.REF_USER = @REF_USER
|
|
12
|
+
AND (U.ROOT_ADMIN = 'OUI'
|
|
13
|
+
OR (S.REF_SOCIETE = @REF_SOCIETE AND US.ADMIN_USER = 'OUI'))
|
|
14
|
+
) OR EXISTS (
|
|
15
|
+
SELECT 1
|
|
16
|
+
FROM DROITSMK_LA.dbo.T_USERS U
|
|
17
|
+
INNER JOIN DROITSMK_LA.dbo.USER_TABLES UT ON U.REF_USER = UT.REF_USER
|
|
18
|
+
INNER JOIN DROITSMK_LA.dbo.T_TABLES T ON UT.TABLE_NAME_ID = T.TABLE_NAME_ID
|
|
19
|
+
WHERE U.REF_USER = @REF_USER
|
|
20
|
+
AND T.TABLE_NAME = @TABLE_NAME
|
|
21
|
+
AND T.TABLE_SELECTEUR = @TABLE_SELECTEUR
|
|
22
|
+
AND UT.REF_SOCIETE = @REF_SOCIETE
|
|
23
|
+
AND ((UT.D_CONSULTATION = 1 AND @Access = 1)
|
|
24
|
+
OR (UT.D_VALIDATION = 1 AND @Access = 2)
|
|
25
|
+
OR (UT.D_AJOUT = 1 AND @Access = 4)
|
|
26
|
+
OR (UT.D_MODIFICATION = 1 AND @Access = 8)
|
|
27
|
+
OR (UT.D_SUPRESSION = 1 AND @Access = 16))
|
|
28
|
+
) THEN 1 ELSE 0 END AS smallint) AS D_Result;`;
|
|
29
|
+
|
|
30
|
+
function text50(value, name) {
|
|
31
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.length > 50 ||
|
|
32
|
+
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
33
|
+
throw new Error(`${name} must be a nonempty string of at most 50 characters without control characters`);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function integer(value, name, min, max) {
|
|
38
|
+
if ((typeof value !== 'number' && typeof value !== 'string') ||
|
|
39
|
+
(typeof value === 'string' && !/^-?\d+$/.test(value))) {
|
|
40
|
+
throw new Error(`${name} must be an integer`);
|
|
41
|
+
}
|
|
42
|
+
const number = Number(value);
|
|
43
|
+
if (!Number.isSafeInteger(number) || number < min || number > max) {
|
|
44
|
+
throw new Error(`${name} must be between ${min} and ${max}`);
|
|
45
|
+
}
|
|
46
|
+
return number;
|
|
47
|
+
}
|
|
48
|
+
function validateInput(raw) {
|
|
49
|
+
const value = {
|
|
50
|
+
refUser: text50(raw.refUser, 'REF_USER'),
|
|
51
|
+
refSociete: text50(raw.refSociete, 'REF_SOCIETE'),
|
|
52
|
+
tableName: text50(raw.tableName, 'TABLE_NAME'),
|
|
53
|
+
tableSelecteur: integer(raw.tableSelecteur, 'TABLE_SELECTEUR', -32768, 32767),
|
|
54
|
+
access: integer(raw.access, 'Access', 1, 16),
|
|
55
|
+
};
|
|
56
|
+
if (![1, 2, 4, 8, 16].includes(value.access)) {
|
|
57
|
+
throw new Error('Access must be exactly one of 1, 2, 4, 8, 16; combined masks are not supported');
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function connectionConfig(c) {
|
|
62
|
+
const config = {
|
|
63
|
+
server: c.server,
|
|
64
|
+
database: c.database || 'master',
|
|
65
|
+
user: c.user,
|
|
66
|
+
password: c.password,
|
|
67
|
+
port: c.port ?? 1433,
|
|
68
|
+
connectionTimeout: c.connectTimeout ?? 15000,
|
|
69
|
+
requestTimeout: c.requestTimeout ?? 15000,
|
|
70
|
+
options: {
|
|
71
|
+
encrypt: c.tls ?? true,
|
|
72
|
+
trustServerCertificate: c.allowUnauthorizedCerts ?? false,
|
|
73
|
+
tdsVersion: c.tdsVersion || '7_4',
|
|
74
|
+
},
|
|
75
|
+
pool: { min: 0, max: 1 },
|
|
76
|
+
};
|
|
77
|
+
if (c.domain) config.domain = c.domain;
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
async function checkAuthorization(pool, sql, value) {
|
|
81
|
+
const request = pool.request();
|
|
82
|
+
request.input('REF_USER', sql.VarChar(50), value.refUser);
|
|
83
|
+
request.input('REF_SOCIETE', sql.VarChar(50), value.refSociete);
|
|
84
|
+
request.input('TABLE_NAME', sql.VarChar(50), value.tableName);
|
|
85
|
+
request.input('TABLE_SELECTEUR', sql.SmallInt, value.tableSelecteur);
|
|
86
|
+
request.input('Access', sql.SmallInt, value.access);
|
|
87
|
+
const result = await request.query(AUTHORIZATION_SQL);
|
|
88
|
+
const rows = result.recordset;
|
|
89
|
+
if (!Array.isArray(rows) || rows.length !== 1 || ![0, 1].includes(rows[0].D_Result)) {
|
|
90
|
+
throw new Error('Unexpected authorization response from SQL Server');
|
|
91
|
+
}
|
|
92
|
+
return rows[0].D_Result;
|
|
93
|
+
}
|
|
94
|
+
module.exports = { AUTHORIZATION_SQL, validateInput, connectionConfig, checkAuthorization };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#005baa" d="M32 3 57 13v18c0 15-13 25-25 30C20 56 7 46 7 31V13z"/><path fill="none" stroke="white" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" d="m20 31 8 8 17-18"/></svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-node-authorization",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Somepharm SQL Server authorization with authorized and denied outputs",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"keywords": ["n8n-community-node-package"],
|
|
7
|
+
"engines": { "node": ">=20" },
|
|
8
|
+
"files": ["dist", "README.md"],
|
|
9
|
+
"scripts": { "test": "node --test test/*.test.js" },
|
|
10
|
+
"n8n": { "n8nNodesApiVersion": 1, "nodes": ["dist/nodes/SomepharmAuthorization/SomepharmAuthorization.node.js"] },
|
|
11
|
+
"dependencies": { "mssql": "12.7.2" },
|
|
12
|
+
"peerDependencies": { "n8n-workflow": "*" },
|
|
13
|
+
"devDependencies": { "n8n-workflow": "2.16.0" }
|
|
14
|
+
}
|