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
+ }