iobroker.eos-admin 7.9.23 → 7.9.26
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 +26 -1
- package/admin/jsonConfig.json5 +143 -0
- package/adminWww/assets/Adapters-B5_jQ7DE.js +1 -1
- package/adminWww/assets/Instances-YdaGnS5a.js +1 -1
- package/adminWww/css/eos-branding.css +245 -0
- package/adminWww/index.html +3 -2
- package/adminWww/js/eos-branding.js +221 -1
- package/adminWww/js/eos-security-ui.js +212 -0
- package/build/lib/testPassword.js.map +1 -1
- package/build/lib/web.js +196 -18
- package/build/lib/web.js.map +1 -1
- package/build/main.js +332 -1
- package/build/main.js.map +1 -1
- package/io-package.json +50 -10
- package/package.json +1 -1
- package/tools/nexowatt-generate-eos-admin-repo-entry.cjs +1 -1
- package/tools/nexowatt-generate-repo-entry.cjs +12 -23
- package/tools/nexowatt-patch-repo.cjs +14 -22
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const VERSION = 'v26-security-visibility-guard';
|
|
5
|
+
const LEGACY_ADMIN = 'admin';
|
|
6
|
+
const LEGACY_ADMIN_INSTANCE = 'admin.0';
|
|
7
|
+
const SECURITY_URL = '/nexowatt/security/session';
|
|
8
|
+
|
|
9
|
+
const state = {
|
|
10
|
+
loaded: false,
|
|
11
|
+
policy: {
|
|
12
|
+
isAdmin: false,
|
|
13
|
+
isEosAdminGroup: false,
|
|
14
|
+
isAdministrator: false,
|
|
15
|
+
hideLegacyAdminFromNonAdmins: true,
|
|
16
|
+
restrictProtectedAdapterControls: true,
|
|
17
|
+
protectedAdapters: ['eos-admin'],
|
|
18
|
+
},
|
|
19
|
+
scheduled: false,
|
|
20
|
+
observer: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
|
|
24
|
+
const normalizeAdapter = value => {
|
|
25
|
+
let adapter = String(value || '').trim().toLowerCase();
|
|
26
|
+
adapter = adapter.replace(/^system\.adapter\./, '').replace(/^iobroker\./, '').replace(/^@nexowatt\/iobroker\./, '').replace(/^@nexowatt\//, '');
|
|
27
|
+
adapter = adapter.replace(/\.\d+$/, '');
|
|
28
|
+
return /^[a-z0-9_-]+$/.test(adapter) ? adapter : '';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isAdminUser = () => !!(state.policy?.isAdmin || state.policy?.isEosAdminGroup || state.policy?.isAdministrator);
|
|
32
|
+
const protectedAdapters = () => new Set((state.policy?.protectedAdapters || []).map(normalizeAdapter).filter(Boolean));
|
|
33
|
+
|
|
34
|
+
const isLegacyAdminId = value => {
|
|
35
|
+
const text = String(value || '').toLowerCase();
|
|
36
|
+
return text === LEGACY_ADMIN || text === LEGACY_ADMIN_INSTANCE || text === 'system.adapter.admin' || text === 'system.adapter.admin.0';
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const isProtectedAdapter = value => {
|
|
40
|
+
const adapter = normalizeAdapter(value);
|
|
41
|
+
return !!adapter && protectedAdapters().has(adapter);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
window.NEXOWATT_EOS_SECURITY = {
|
|
45
|
+
version: VERSION,
|
|
46
|
+
getPolicy: () => state.policy,
|
|
47
|
+
isAdminUser,
|
|
48
|
+
isProtectedAdapter,
|
|
49
|
+
shouldBlockAdapterDelete(adapterName) {
|
|
50
|
+
if (isAdminUser()) return false;
|
|
51
|
+
const adapter = normalizeAdapter(adapterName);
|
|
52
|
+
return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
|
|
53
|
+
},
|
|
54
|
+
shouldBlockInstanceDelete(instanceIdOrAdapter) {
|
|
55
|
+
if (isAdminUser()) return false;
|
|
56
|
+
const raw = String(instanceIdOrAdapter || '').replace(/^system\.adapter\./, '');
|
|
57
|
+
const adapter = normalizeAdapter(raw);
|
|
58
|
+
return adapter === LEGACY_ADMIN || isProtectedAdapter(adapter);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const closestPanel = el => {
|
|
63
|
+
if (!el || !el.closest) return null;
|
|
64
|
+
return el.closest('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root, .MuiBox-root');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const markHidden = el => {
|
|
68
|
+
if (!el || el.classList?.contains('eos-security-keep-visible')) return;
|
|
69
|
+
el.classList.add('eos-security-hidden');
|
|
70
|
+
el.setAttribute('aria-hidden', 'true');
|
|
71
|
+
el.style.display = 'none';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const elementHasLegacyAdminIcon = el => {
|
|
75
|
+
if (!el?.querySelectorAll) return false;
|
|
76
|
+
return Array.from(el.querySelectorAll('img,[src]')).some(img => {
|
|
77
|
+
const src = String(img.getAttribute('src') || '').toLowerCase();
|
|
78
|
+
return /(?:^|\/)adapter\/admin\//.test(src) || /(?:^|\/)admin\/(?:admin\.(?:png|svg)|admin\.svg)/.test(src);
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const elementTextMatchesLegacyAdmin = el => {
|
|
83
|
+
const text = normalize(el?.textContent || '').toLowerCase();
|
|
84
|
+
if (!text) return false;
|
|
85
|
+
if (text.includes('eos-admin') || text.includes('eos admin')) return false;
|
|
86
|
+
return /\badmin\.0\b/.test(text) || /system\.adapter\.admin(?:\.0)?\b/.test(text) || /\biobroker\.admin\b/.test(text);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const hideLegacyAdminPanels = () => {
|
|
90
|
+
if (!state.policy?.hideLegacyAdminFromNonAdmins || isAdminUser()) return;
|
|
91
|
+
const candidates = new Set();
|
|
92
|
+
document.querySelectorAll('img,[src]').forEach(img => {
|
|
93
|
+
if (elementHasLegacyAdminIcon(img.parentElement || img)) candidates.add(closestPanel(img) || img);
|
|
94
|
+
});
|
|
95
|
+
document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(el => {
|
|
96
|
+
if (elementHasLegacyAdminIcon(el) || elementTextMatchesLegacyAdmin(el)) candidates.add(el);
|
|
97
|
+
});
|
|
98
|
+
candidates.forEach(el => {
|
|
99
|
+
const text = normalize(el.textContent || '').toLowerCase();
|
|
100
|
+
if (text.includes('eos-admin') || text.includes('eos admin')) return;
|
|
101
|
+
markHidden(el);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const hideProtectedDeleteControls = () => {
|
|
106
|
+
if (!state.policy?.restrictProtectedAdapterControls || isAdminUser()) return;
|
|
107
|
+
const protectedSet = protectedAdapters();
|
|
108
|
+
if (!protectedSet.size) return;
|
|
109
|
+
|
|
110
|
+
document.querySelectorAll('.MuiCard-root, .MuiPaper-root, [role="row"], tr, .MuiListItem-root').forEach(panel => {
|
|
111
|
+
let adapter = '';
|
|
112
|
+
const icon = panel.querySelector('img[src*="/adapter/"], img[src*="adapter/"]');
|
|
113
|
+
const src = icon ? String(icon.getAttribute('src') || '') : '';
|
|
114
|
+
const match = src.match(/adapter\/([^\/]+)\//i);
|
|
115
|
+
if (match) adapter = normalizeAdapter(match[1]);
|
|
116
|
+
const text = normalize(panel.textContent || '').toLowerCase();
|
|
117
|
+
if (!adapter) {
|
|
118
|
+
for (const protectedName of protectedSet) {
|
|
119
|
+
if (new RegExp(`\\b${protectedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.\\d+)?\\b`, 'i').test(text)) {
|
|
120
|
+
adapter = protectedName;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!adapter || !protectedSet.has(adapter)) return;
|
|
126
|
+
panel.classList.add('eos-security-protected-adapter');
|
|
127
|
+
panel.querySelectorAll('button,[role="button"],a').forEach(button => {
|
|
128
|
+
const label = normalize(`${button.textContent || ''} ${button.getAttribute('title') || ''} ${button.getAttribute('aria-label') || ''}`).toLowerCase();
|
|
129
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
|
|
130
|
+
button.classList.add('eos-security-hidden-delete');
|
|
131
|
+
button.setAttribute('disabled', 'disabled');
|
|
132
|
+
button.setAttribute('aria-disabled', 'true');
|
|
133
|
+
button.style.display = 'none';
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
document.querySelectorAll('[role="menuitem"], .MuiMenuItem-root').forEach(item => {
|
|
139
|
+
const text = normalize(item.textContent || '').toLowerCase();
|
|
140
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(text)) {
|
|
141
|
+
// Context menus cannot always be mapped back to the originating card. For protected NexoWatt systems,
|
|
142
|
+
// deletion through pop-up menus is blocked by the command guard and visually hidden for non-admin users.
|
|
143
|
+
item.classList.add('eos-security-hidden-delete');
|
|
144
|
+
item.style.display = 'none';
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const applyPolicyToDom = () => {
|
|
150
|
+
document.documentElement.classList.toggle('eos-security-admin-user', isAdminUser());
|
|
151
|
+
document.documentElement.classList.toggle('eos-security-non-admin-user', !isAdminUser());
|
|
152
|
+
hideLegacyAdminPanels();
|
|
153
|
+
hideProtectedDeleteControls();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const scheduleApply = () => {
|
|
157
|
+
if (state.scheduled) return;
|
|
158
|
+
state.scheduled = true;
|
|
159
|
+
const run = () => {
|
|
160
|
+
state.scheduled = false;
|
|
161
|
+
applyPolicyToDom();
|
|
162
|
+
};
|
|
163
|
+
if ('requestIdleCallback' in window) window.requestIdleCallback(run, { timeout: 600 });
|
|
164
|
+
else window.requestAnimationFrame(run);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const loadPolicy = async () => {
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(SECURITY_URL, { credentials: 'same-origin', cache: 'no-store' });
|
|
170
|
+
const policy = await response.json();
|
|
171
|
+
state.policy = { ...state.policy, ...policy };
|
|
172
|
+
state.loaded = true;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
state.loaded = false;
|
|
175
|
+
state.policy = { ...state.policy, isAdmin: false, isEosAdminGroup: false, isAdministrator: false };
|
|
176
|
+
console.warn('[NexoWatt EOS] Cannot read security policy, using safe non-admin UI mode:', e);
|
|
177
|
+
}
|
|
178
|
+
scheduleApply();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const installObserver = () => {
|
|
182
|
+
if (state.observer) return;
|
|
183
|
+
state.observer = new MutationObserver(() => scheduleApply());
|
|
184
|
+
state.observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
document.addEventListener('click', event => {
|
|
188
|
+
const target = event.target?.closest?.('button,[role="button"],a,[role="menuitem"],.MuiMenuItem-root');
|
|
189
|
+
if (!target || isAdminUser()) return;
|
|
190
|
+
const label = normalize(`${target.textContent || ''} ${target.getAttribute?.('title') || ''} ${target.getAttribute?.('aria-label') || ''}`).toLowerCase();
|
|
191
|
+
if (/löschen|delete|remove|deinstall|uninstall/.test(label)) {
|
|
192
|
+
target.classList.add('eos-security-hidden-delete');
|
|
193
|
+
target.style.display = 'none';
|
|
194
|
+
}
|
|
195
|
+
}, true);
|
|
196
|
+
|
|
197
|
+
if (document.readyState === 'loading') {
|
|
198
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
199
|
+
loadPolicy();
|
|
200
|
+
installObserver();
|
|
201
|
+
[300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
|
|
202
|
+
}, { once: true });
|
|
203
|
+
} else {
|
|
204
|
+
loadPolicy();
|
|
205
|
+
installObserver();
|
|
206
|
+
[300, 1000, 2500, 5000].forEach(ms => window.setTimeout(scheduleApply, ms));
|
|
207
|
+
}
|
|
208
|
+
window.addEventListener('hashchange', () => {
|
|
209
|
+
loadPolicy();
|
|
210
|
+
scheduleApply();
|
|
211
|
+
});
|
|
212
|
+
})();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,
|
|
1
|
+
{"version":3,"file":"testPassword.js","sourceRoot":"./src/","sources":["lib/testPassword.ts"],"names":[],"mappings":";;AAAA,iDAAgD;AAChD,qDAA6E;AAC7E,6CAAuC;AAEvC,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,kDAAkD;AAClD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,IAAI,sBAAQ,CAAC;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;QACnC,IAAI,UAAU,EAAE,CAAC;YACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;aAAM,CAAC;YACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC;QACD,QAAQ,EAAE,CAAC;IACf,CAAC;CACJ,CAAC,CAAC;AAEH,mEAAmE;AACnE,IAAA,wCAAuB,GAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;IACnC,IAAI,KAAK,EAAE,CAAC;QACR,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACnE,qBAAqB;QACrB,MAAM,EAAE,GAAG,IAAA,+BAAe,EAAC;YACvB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,aAAa;YACrB,QAAQ,EAAE,IAAI;SACjB,CAAC,CAAC;QACH,EAAE,CAAC,QAAQ,CAAC,2BAA2B,KAAK,CAAC,KAAK,kBAAkB,EAAE,CAAC,QAAgB,EAAE,EAAE;YACvF,UAAU,GAAG,KAAK,CAAC;YACnB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACtB,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;gBACvC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;YACD,EAAE,CAAC,QAAQ,CAAC,4BAA4B,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,cAAsB,EAAE,EAAE;gBAChF,UAAU,GAAG,KAAK,CAAC;gBACnB,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACtE,IAAI,QAAQ,KAAK,cAAc,EAAE,CAAC;oBAC9B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;oBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpB,CAAC;gBACD,EAAE,CAAC,KAAK,EAAE,CAAC;gBACX,KAAK,IAAA,iCAAgB,EAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBACvE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACrD,CAAC,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;YACH,UAAU,GAAG,IAAI,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,UAAU,GAAG,IAAI,CAAC;IACtB,CAAC;SAAM,CAAC;QACJ,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/build/lib/web.js
CHANGED
|
@@ -341,6 +341,159 @@ class Web {
|
|
|
341
341
|
resetIndexHtml() {
|
|
342
342
|
this.indexHTML = '';
|
|
343
343
|
}
|
|
344
|
+
|
|
345
|
+
normalizeEosGroupId(value) {
|
|
346
|
+
if (typeof value !== 'string') return null;
|
|
347
|
+
let group = value.trim();
|
|
348
|
+
if (!group) return null;
|
|
349
|
+
if (!group.startsWith('system.group.')) group = `system.group.${group.replace(/^group\./, '')}`;
|
|
350
|
+
return /^system\.group\.[a-z0-9_-]+$/i.test(group) ? group : null;
|
|
351
|
+
}
|
|
352
|
+
normalizeEosUserId(value) {
|
|
353
|
+
if (!value) return null;
|
|
354
|
+
let user;
|
|
355
|
+
if (typeof value === 'string') user = value;
|
|
356
|
+
else if (typeof value === 'object') {
|
|
357
|
+
const raw = value;
|
|
358
|
+
user = String(raw.id || raw._id || raw.user || raw.name || '');
|
|
359
|
+
}
|
|
360
|
+
else user = String(value);
|
|
361
|
+
user = user.trim();
|
|
362
|
+
if (!user) return null;
|
|
363
|
+
if (!user.startsWith('system.user.')) user = `system.user.${user.replace(/^user\./, '')}`;
|
|
364
|
+
return /^system\.user\.[a-z0-9_-]+$/i.test(user) ? user : null;
|
|
365
|
+
}
|
|
366
|
+
normalizeEosAdapterName(value) {
|
|
367
|
+
if (typeof value !== 'string') return null;
|
|
368
|
+
let adapter = value.trim().toLowerCase();
|
|
369
|
+
if (!adapter) return null;
|
|
370
|
+
adapter = adapter
|
|
371
|
+
.replace(/^system\.adapter\./, '')
|
|
372
|
+
.replace(/^iobroker\./, '')
|
|
373
|
+
.replace(/^@nexowatt\/iobroker\./, '')
|
|
374
|
+
.replace(/^@nexowatt\//, '')
|
|
375
|
+
.replace(/\.\d+$/, '');
|
|
376
|
+
return /^[a-z0-9_-]+$/.test(adapter) ? adapter : null;
|
|
377
|
+
}
|
|
378
|
+
getEosSecurityAdminGroups() {
|
|
379
|
+
const configured = this.settings.eosAdminOnlyGroups || this.settings.eosSecurityAdminGroups || this.settings.eosAdminOnlyGroup;
|
|
380
|
+
const groups = new Set();
|
|
381
|
+
const add = value => {
|
|
382
|
+
if (!value) return;
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
value.forEach(add);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (typeof value === 'object') {
|
|
388
|
+
if (value.enabled === false) return;
|
|
389
|
+
add(value.group || value.id || value.name);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const group = this.normalizeEosGroupId(String(value));
|
|
393
|
+
if (group) groups.add(group);
|
|
394
|
+
};
|
|
395
|
+
add(configured);
|
|
396
|
+
groups.add('system.group.administrator');
|
|
397
|
+
return [...groups].sort();
|
|
398
|
+
}
|
|
399
|
+
getEosProtectedAdapterNames() {
|
|
400
|
+
const names = new Set(['eos-admin']);
|
|
401
|
+
const add = value => {
|
|
402
|
+
if (!value) return;
|
|
403
|
+
if (Array.isArray(value)) {
|
|
404
|
+
value.forEach(add);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (typeof value === 'object') {
|
|
408
|
+
if (value.enabled === false) return;
|
|
409
|
+
add(value.adapter || value.name);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const adapter = this.normalizeEosAdapterName(String(value));
|
|
413
|
+
if (adapter) names.add(adapter);
|
|
414
|
+
};
|
|
415
|
+
if (this.settings.eosProtectAdapterDeletion !== false) add(this.settings.eosProtectedAdapters);
|
|
416
|
+
return [...names].sort();
|
|
417
|
+
}
|
|
418
|
+
async getEosGroupsForUser(userId) {
|
|
419
|
+
const groups = new Set();
|
|
420
|
+
try {
|
|
421
|
+
const result = await this.adapter.getObjectViewAsync('system', 'group', {
|
|
422
|
+
startkey: 'system.group.',
|
|
423
|
+
endkey: 'system.group.香',
|
|
424
|
+
});
|
|
425
|
+
for (const row of result.rows) {
|
|
426
|
+
const group = row.value || row.doc;
|
|
427
|
+
const members = Array.isArray(group?.common?.members) ? group.common.members : [];
|
|
428
|
+
if (members.includes(userId)) groups.add(row.id);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
this.adapter.log.debug(`Cannot resolve EOS security groups for ${userId}: ${e instanceof Error ? e.message : e}`);
|
|
433
|
+
}
|
|
434
|
+
return [...groups].sort();
|
|
435
|
+
}
|
|
436
|
+
readAccessTokenFromRequest(req) {
|
|
437
|
+
const cookieHeader = req.headers.cookie || '';
|
|
438
|
+
const accessCookie = cookieHeader
|
|
439
|
+
.split(';')
|
|
440
|
+
.map(cookie => cookie.trim())
|
|
441
|
+
.find(cookie => cookie.startsWith('access_token='));
|
|
442
|
+
let token = accessCookie ? accessCookie.split('=').slice(1).join('=') : '';
|
|
443
|
+
if (!token && req.headers.authorization?.startsWith('Bearer ')) token = req.headers.authorization.substring('Bearer '.length);
|
|
444
|
+
if (!token && req.query?.token) token = req.query.token;
|
|
445
|
+
if (!token) return null;
|
|
446
|
+
try {
|
|
447
|
+
return decodeURIComponent(token);
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return token;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
readSession(id) {
|
|
454
|
+
return new Promise(resolve => this.adapter.getSession(id, token => resolve(token)));
|
|
455
|
+
}
|
|
456
|
+
async readEosCurrentUser(req) {
|
|
457
|
+
const fromRequest = this.normalizeEosUserId(req.user);
|
|
458
|
+
if (fromRequest) return fromRequest;
|
|
459
|
+
if (!this.settings.auth) return this.normalizeEosUserId(this.settings.defaultUser) || 'system.user.admin';
|
|
460
|
+
const token = this.readAccessTokenFromRequest(req);
|
|
461
|
+
if (!token) return null;
|
|
462
|
+
const candidates = new Set();
|
|
463
|
+
candidates.add(token);
|
|
464
|
+
candidates.add(token.startsWith('a:') ? token : `a:${token}`);
|
|
465
|
+
if (token.length > 1) candidates.add(`a:${token[1]}`);
|
|
466
|
+
for (const id of candidates) {
|
|
467
|
+
const session = await this.readSession(id).catch(() => null);
|
|
468
|
+
const user = this.normalizeEosUserId(session?.user);
|
|
469
|
+
if (user) return user;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
async sendEosSecuritySession(req, res) {
|
|
474
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
475
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
476
|
+
const userId = await this.readEosCurrentUser(req);
|
|
477
|
+
const groups = userId ? await this.getEosGroupsForUser(userId) : [];
|
|
478
|
+
const adminGroups = this.getEosSecurityAdminGroups();
|
|
479
|
+
const isAdministrator = userId === 'system.user.admin' || groups.includes('system.group.administrator');
|
|
480
|
+
const isEosAdminGroup = isAdministrator || groups.some(group => adminGroups.includes(group));
|
|
481
|
+
res.status(200).json({
|
|
482
|
+
user: userId,
|
|
483
|
+
groups,
|
|
484
|
+
adminGroups,
|
|
485
|
+
isAdministrator,
|
|
486
|
+
isEosAdminGroup,
|
|
487
|
+
isAdmin: isEosAdminGroup,
|
|
488
|
+
hideLegacyAdminForNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
|
|
489
|
+
hideLegacyAdminFromNonAdmins: this.settings.eosHideLegacyAdminForNonAdmins !== false && this.settings.eosHideLegacyAdminFromNonAdmins !== false,
|
|
490
|
+
restrictProtectedAdapterControls: this.settings.eosRestrictProtectedAdapterControls !== false && this.settings.eosApplyAdminOnlyAclToProtectedAdapters !== false,
|
|
491
|
+
legacyAdminAdapter: 'admin',
|
|
492
|
+
legacyAdminInstance: 'admin.0',
|
|
493
|
+
protectedAdapters: this.getEosProtectedAdapterNames(),
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
344
497
|
/**
|
|
345
498
|
* Initialize the server
|
|
346
499
|
*/
|
|
@@ -462,6 +615,24 @@ class Web {
|
|
|
462
615
|
}
|
|
463
616
|
res.json({ error: 'Cannot find session' });
|
|
464
617
|
});
|
|
618
|
+
this.server.app.get(/.*\/nexowatt\/security\/(?:context|session)$/, (req, res) => {
|
|
619
|
+
void this.sendEosSecuritySession(req, res).catch(e => {
|
|
620
|
+
this.adapter.log.warn(`Cannot create NexoWatt security context: ${e instanceof Error ? e.message : e}`);
|
|
621
|
+
res.status(200).json({
|
|
622
|
+
user: null,
|
|
623
|
+
groups: [],
|
|
624
|
+
adminGroups: ['system.group.administrator'],
|
|
625
|
+
isAdministrator: false,
|
|
626
|
+
isEosAdminGroup: false,
|
|
627
|
+
isAdmin: false,
|
|
628
|
+
hideLegacyAdminForNonAdmins: true,
|
|
629
|
+
hideLegacyAdminFromNonAdmins: true,
|
|
630
|
+
restrictProtectedAdapterControls: true,
|
|
631
|
+
protectedAdapters: ['eos-admin'],
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
465
636
|
this.server.app.get('/logout', (req, res) => {
|
|
466
637
|
const isDev = req.url.includes('?dev');
|
|
467
638
|
let origin = req.url.split('origin=')[1];
|
|
@@ -536,6 +707,27 @@ class Web {
|
|
|
536
707
|
else {
|
|
537
708
|
this.server.app.get('/logout', (_req, res) => res.redirect('/'));
|
|
538
709
|
}
|
|
710
|
+
const sendSecuritySession = (req, res) => {
|
|
711
|
+
void this.sendEosSecuritySession(req, res).catch(e => {
|
|
712
|
+
this.adapter.log.warn(`Cannot read EOS security session: ${e instanceof Error ? e.message : e}`);
|
|
713
|
+
res.status(200).json({
|
|
714
|
+
user: null,
|
|
715
|
+
groups: [],
|
|
716
|
+
adminGroups: ['system.group.administrator'],
|
|
717
|
+
isAdministrator: false,
|
|
718
|
+
isEosAdminGroup: false,
|
|
719
|
+
isAdmin: false,
|
|
720
|
+
hideLegacyAdminForNonAdmins: true,
|
|
721
|
+
hideLegacyAdminFromNonAdmins: true,
|
|
722
|
+
restrictProtectedAdapterControls: true,
|
|
723
|
+
protectedAdapters: ['eos-admin'],
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
this.server.app.get('/eos/security/status', sendSecuritySession);
|
|
728
|
+
this.server.app.get('/nexowatt/security/session', sendSecuritySession);
|
|
729
|
+
this.server.app.get('/nexowatt/security/context', sendSecuritySession);
|
|
730
|
+
|
|
539
731
|
this.server.app.get('/iobroker_check.html', (_req, res) => {
|
|
540
732
|
res.status(200).send('ioBroker');
|
|
541
733
|
});
|
|
@@ -1020,34 +1212,20 @@ class Web {
|
|
|
1020
1212
|
}
|
|
1021
1213
|
catch (err) {
|
|
1022
1214
|
this.adapter.log.error(`Cannot create web-server: ${err}`);
|
|
1023
|
-
|
|
1024
|
-
this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1025
|
-
}
|
|
1026
|
-
else {
|
|
1027
|
-
process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1028
|
-
}
|
|
1215
|
+
this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1029
1216
|
return;
|
|
1030
1217
|
}
|
|
1031
1218
|
if (!this.server.server) {
|
|
1032
1219
|
this.adapter.log.error(`Cannot create web-server`);
|
|
1033
|
-
|
|
1034
|
-
this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1035
|
-
}
|
|
1036
|
-
else {
|
|
1037
|
-
process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1038
|
-
}
|
|
1220
|
+
this.adapter.terminate(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1039
1221
|
return;
|
|
1040
1222
|
}
|
|
1041
1223
|
this.server.server.__server = this.server;
|
|
1042
1224
|
}
|
|
1043
1225
|
else {
|
|
1044
1226
|
this.adapter.log.error('port missing');
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
process.exit(adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1050
|
-
}
|
|
1227
|
+
this.adapter.terminate('port missing', adapter_core_1.EXIT_CODES.ADAPTER_REQUESTED_TERMINATION);
|
|
1228
|
+
return;
|
|
1051
1229
|
}
|
|
1052
1230
|
const systemConfig = await this.adapter.getForeignObjectAsync('system.config');
|
|
1053
1231
|
this.systemConfig = systemConfig || {};
|