Skip to content

Commit e19bfc3

Browse files
committed
Updated to 1.5.17: Security impersonation
- Added security impersonation to the API token that gets passed into reads/writes/browses
1 parent 8397306 commit e19bfc3

5 files changed

Lines changed: 132 additions & 17 deletions

File tree

IgnitionNodeRED-build/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<moduleId>org.imdc.nodered.IgnitionNodeRED</moduleId>
4848
<moduleName>${module-name}</moduleName>
4949
<moduleDescription>${module-description}</moduleDescription>
50-
<moduleVersion>1.5.16.${timestamp}</moduleVersion>
50+
<moduleVersion>1.5.17.${timestamp}</moduleVersion>
5151
<requiredIgnitionVersion>${ignition-platform-version}</requiredIgnitionVersion>
5252
<requiredFrameworkVersion>8</requiredFrameworkVersion>
5353
<licenseFile>license.html</licenseFile>

IgnitionNodeRED-gateway/src/main/java/org/imdc/nodered/NodeREDAPITokens.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,55 @@ public void onValidate(SFieldMeta field, SRecordInstance instance) throws SExcep
4343
public static final ReferenceField<AuditProfileRecord> AuditProfile =
4444
new ReferenceField<AuditProfileRecord>(META, AuditProfileRecord.META, "AuditProfile", AuditProfileId);
4545

46+
public static final StringField SecurityLevels = new StringField(META, "SecurityLevels").addValidator(new SValidatorI() {
47+
@Override
48+
public void onValidate(SFieldMeta field, SRecordInstance instance) throws SException.Validation {
49+
if (!instance.isNull(field)) {
50+
String val = instance.getString(field);
51+
52+
try {
53+
val.split(",");
54+
} catch (Throwable ex) {
55+
throw new SException.Validation("Field " + field + " value must be a comma separated string");
56+
}
57+
}
58+
}
59+
});
60+
public static final StringField Roles = new StringField(META, "Roles").addValidator(new SValidatorI() {
61+
@Override
62+
public void onValidate(SFieldMeta field, SRecordInstance instance) throws SException.Validation {
63+
if (!instance.isNull(field)) {
64+
String val = instance.getString(field);
65+
66+
try {
67+
val.split(",");
68+
} catch (Throwable ex) {
69+
throw new SException.Validation("Field " + field + " value must be a comma separated string");
70+
}
71+
}
72+
}
73+
});
74+
public static final StringField Zones = new StringField(META, "Zones").addValidator(new SValidatorI() {
75+
@Override
76+
public void onValidate(SFieldMeta field, SRecordInstance instance) throws SException.Validation {
77+
if (!instance.isNull(field)) {
78+
String val = instance.getString(field);
79+
80+
try {
81+
val.split(",");
82+
} catch (Throwable ex) {
83+
throw new SException.Validation("Field " + field + " value must be a comma separated string");
84+
}
85+
}
86+
}
87+
});
88+
4689
public static final BooleanField Enabled = new BooleanField(META, "Enabled").setDefault(true);
4790

91+
public static final Category SettingsCategory = new Category("NodeRED.Settings", 125).include(Name, APIToken, Secret, AuditProfile, Enabled);
92+
93+
public static final Category ImpersonateCategory = new Category("NodeRED.Impersonate", 126).include(SecurityLevels, Roles, Zones);
94+
4895
static {
4996
Name.getFormMeta().setFieldNameKey("NodeRED.Name.Name");
5097
Name.getFormMeta().setFieldDescriptionKey("NodeRED.Name.Desc");
@@ -55,6 +102,12 @@ public void onValidate(SFieldMeta field, SRecordInstance instance) throws SExcep
55102
Secret.getFormMeta().setEditorSource(PasswordEditorSource.getSharedInstance());
56103
AuditProfile.getFormMeta().setFieldNameKey("NodeRED.AuditProfile.Name");
57104
AuditProfile.getFormMeta().setFieldDescriptionKey("NodeRED.AuditProfile.Desc");
105+
SecurityLevels.getFormMeta().setFieldNameKey("NodeRED.SecurityLevels.Name");
106+
SecurityLevels.getFormMeta().setFieldDescriptionKey("NodeRED.SecurityLevels.Desc");
107+
Roles.getFormMeta().setFieldNameKey("NodeRED.Roles.Name");
108+
Roles.getFormMeta().setFieldDescriptionKey("NodeRED.Roles.Desc");
109+
Zones.getFormMeta().setFieldNameKey("NodeRED.Zones.Name");
110+
Zones.getFormMeta().setFieldDescriptionKey("NodeRED.Zones.Desc");
58111
Enabled.getFormMeta().setFieldNameKey("NodeRED.Enabled.Name");
59112
Enabled.getFormMeta().setFieldDescriptionKey("NodeRED.Enabled.Desc");
60113
}
@@ -87,4 +140,16 @@ public String getSecret() {
87140
public Long getAuditProfileId() {
88141
return getLong(AuditProfileId);
89142
}
143+
144+
public String getSecurityLevels() {
145+
return getString(SecurityLevels);
146+
}
147+
148+
public String getRoles() {
149+
return getString(Roles);
150+
}
151+
152+
public String getZones() {
153+
return getString(Zones);
154+
}
90155
}

IgnitionNodeRED-gateway/src/main/java/org/imdc/nodered/servlet/APITokenValidation.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
public class APITokenValidation {
99
private boolean success;
10-
private String errorMessage, tokenName, token, auditProfileName;
10+
private String errorMessage, tokenName, token, auditProfileName, securityLevels, roles, zones;
1111

12-
public APITokenValidation(boolean success, String errorMessage, String tokenName, String token, String auditProfileName) {
12+
public APITokenValidation(boolean success, String errorMessage, String tokenName, String token, String auditProfileName, String securityLevels, String roles, String zones) {
1313
this.success = success;
1414
this.errorMessage = errorMessage;
1515
this.tokenName = tokenName;
1616
this.token = token;
1717
this.auditProfileName = auditProfileName;
18+
this.securityLevels = securityLevels;
19+
this.roles = roles;
20+
this.zones = zones;
1821
}
1922

2023
public boolean isSuccess() {
@@ -49,6 +52,18 @@ public boolean isAuditProfileDefined() {
4952
return auditProfileName != null;
5053
}
5154

55+
public String getSecurityLevels() {
56+
return securityLevels;
57+
}
58+
59+
public String getRoles() {
60+
return roles;
61+
}
62+
63+
public String getZones() {
64+
return zones;
65+
}
66+
5267
public static APITokenValidation validateToken(GatewayContext context, String apiToken, String secret) {
5368
boolean success = true;
5469
String errorMessage = null;
@@ -79,6 +94,6 @@ public static APITokenValidation validateToken(GatewayContext context, String ap
7994
}
8095
}
8196

82-
return new APITokenValidation(success, errorMessage, r.getName(), r.getAPIToken(), auditProfileName);
97+
return new APITokenValidation(success, errorMessage, r.getName(), r.getAPIToken(), auditProfileName, r.getSecurityLevels(), r.getRoles(), r.getZones());
8398
}
8499
}

IgnitionNodeRED-gateway/src/main/java/org/imdc/nodered/servlet/NodeREDServlet.java

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package org.imdc.nodered.servlet;
22

3+
import com.google.common.collect.ImmutableCollection;
4+
import com.google.common.collect.ImmutableSet;
35
import com.inductiveautomation.ignition.common.Dataset;
46
import com.inductiveautomation.ignition.common.TypeUtilities;
7+
import com.inductiveautomation.ignition.common.auth.security.level.SecurityLevelConfig;
58
import com.inductiveautomation.ignition.common.browsing.BrowseFilter;
69
import com.inductiveautomation.ignition.common.browsing.Results;
710
import com.inductiveautomation.ignition.common.model.values.QualifiedValue;
811
import com.inductiveautomation.ignition.common.model.values.QualityCode;
912
import com.inductiveautomation.ignition.common.tags.browsing.NodeDescription;
13+
import com.inductiveautomation.ignition.common.tags.model.SecurityContext;
1014
import com.inductiveautomation.ignition.common.tags.model.TagPath;
1115
import com.inductiveautomation.ignition.common.tags.paths.parser.TagPathParser;
1216
import com.inductiveautomation.ignition.common.util.AuditStatus;
@@ -95,6 +99,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
9599
String apiToken = input.getString("apiToken");
96100
String secret = input.getString("secret");
97101
APITokenValidation validation = APITokenValidation.validateToken(context, apiToken, secret);
102+
SecurityContext securityContext = getSecurityContext(validation);
98103

99104
if (validation.isSuccess()) {
100105
String command = input.getString("command");
@@ -143,9 +148,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
143148

144149
if (errorMessage == null) {
145150
if (command.equals("tagRead")) {
146-
tagRead(context, tagPaths, result);
151+
tagRead(context, tagPaths, securityContext, result);
147152
} else if (command.equals("tagBrowse")) {
148-
tagBrowse(context, tagPaths, result);
153+
tagBrowse(context, tagPaths, securityContext, result);
149154
} else if (command.equals("tagWrite")) {
150155
List<Object> values = new ArrayList<>();
151156
if (input.has("value")) {
@@ -168,7 +173,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
168173
if (values.size() != tagPaths.size()) {
169174
errorMessage = "Number of tag paths (" + tagPaths.size() + ") does not match values (" + values.size() + ")";
170175
} else {
171-
tagWrite(context, tagPaths, values, result, ipAddress, validation);
176+
tagWrite(context, tagPaths, values, securityContext, result, ipAddress, validation);
172177
}
173178
}
174179
}
@@ -214,6 +219,28 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
214219
}
215220
}
216221

222+
private SecurityContext getSecurityContext(APITokenValidation validation) {
223+
if (validation.getSecurityLevels() != null && !validation.getSecurityLevels().isBlank()) {
224+
String[][] paths = Arrays.stream(validation.getSecurityLevels().split(",")).map(r -> Arrays.stream(r.trim().split("/")).toArray(String[]::new)).toArray(String[][]::new);
225+
return SecurityContext.fromSecurityLevels(SecurityLevelConfig.fromPaths(paths));
226+
} else if ((validation.getRoles() != null && !validation.getRoles().isBlank()) || (validation.getZones() != null && !validation.getZones().isBlank())) {
227+
ImmutableCollection<String> roles = ImmutableSet.of();
228+
ImmutableCollection<String> zones = ImmutableSet.of();
229+
230+
if (validation.getRoles() != null && !validation.getRoles().isBlank()) {
231+
roles = ImmutableSet.copyOf(Arrays.stream(validation.getRoles().split(",")).map(String::trim).toArray(String[]::new));
232+
}
233+
234+
if (validation.getZones() != null && !validation.getZones().isBlank()) {
235+
zones = ImmutableSet.copyOf(Arrays.stream(validation.getZones().split(",")).map(String::trim).toArray(String[]::new));
236+
}
237+
238+
return SecurityContext.fromRolesAndZones(roles, zones);
239+
}
240+
241+
return SecurityContext.emptyContext();
242+
}
243+
217244
public static void setTagValue(List<TagPath> tagPaths, List<QualifiedValue> tagValues, JSONObject result) throws JSONException {
218245
if (tagPaths.size() == 1) {
219246
result.put("tagPath", tagPaths.get(0).toStringFull());
@@ -267,9 +294,9 @@ public static void setQuality(JSONObject result, QualityCode qual) throws JSONEx
267294
result.put("quality", quality);
268295
}
269296

270-
private JSONArray browseTags(GatewayContext context, TagPath tagPath) throws JSONException, ExecutionException, InterruptedException, Exception {
297+
private JSONArray browseTags(GatewayContext context, TagPath tagPath, SecurityContext securityContext) throws JSONException, ExecutionException, InterruptedException, Exception {
271298
JSONArray tagsArray = new JSONArray();
272-
Results<NodeDescription> browseResults = context.getTagManager().browseAsync(tagPath, BrowseFilter.NONE).get();
299+
Results<NodeDescription> browseResults = context.getTagManager().browseAsync(tagPath, BrowseFilter.NONE, securityContext).get();
273300
Collection<NodeDescription> tagDescriptions = browseResults.getResults();
274301
if (tagDescriptions != null) {
275302
Iterator<NodeDescription> iterator = tagDescriptions.iterator();
@@ -292,31 +319,31 @@ private JSONArray browseTags(GatewayContext context, TagPath tagPath) throws JSO
292319
return tagsArray;
293320
}
294321

295-
private void tagBrowse(GatewayContext context, final List<TagPath> tagPaths, JSONObject result) throws JSONException, ExecutionException, InterruptedException, Exception {
322+
private void tagBrowse(GatewayContext context, final List<TagPath> tagPaths, SecurityContext securityContext, JSONObject result) throws JSONException, ExecutionException, InterruptedException, Exception {
296323
if (tagPaths.size() == 1) {
297324
logger.info("Browsing " + tagPaths.get(0).toStringFull());
298325
result.put("tagPath", tagPaths.get(0).toStringFull());
299-
result.put("tags", browseTags(context, tagPaths.get(0)));
326+
result.put("tags", browseTags(context, tagPaths.get(0), securityContext));
300327
} else {
301328
JSONArray resultValues = new JSONArray();
302329
for (int i = 0; i < tagPaths.size(); i++) {
303330
JSONObject tagObject = new JSONObject();
304331
TagPath tagPath = tagPaths.get(i);
305332
tagObject.put("tagPath", tagPath.toStringFull());
306-
tagObject.put("tags", browseTags(context, tagPath));
333+
tagObject.put("tags", browseTags(context, tagPath, securityContext));
307334
resultValues.put(tagObject);
308335
}
309336
result.put("values", resultValues);
310337
}
311338
}
312339

313-
private void tagRead(GatewayContext context, final List<TagPath> tagPaths, JSONObject result) throws JSONException, ExecutionException, InterruptedException {
314-
List<QualifiedValue> tagValues = context.getTagManager().readAsync(tagPaths).get();
340+
private void tagRead(GatewayContext context, final List<TagPath> tagPaths, SecurityContext securityContext, JSONObject result) throws JSONException, ExecutionException, InterruptedException {
341+
List<QualifiedValue> tagValues = context.getTagManager().readAsync(tagPaths, securityContext).get();
315342
setTagValue(tagPaths, tagValues, result);
316343
}
317344

318-
private void tagWrite(GatewayContext context, final List<TagPath> tagPaths, final List<Object> writeValues, JSONObject result, String ipAddress, APITokenValidation validation) throws JSONException, ExecutionException, InterruptedException {
319-
List<QualityCode> writeResult = context.getTagManager().writeAsync(tagPaths, writeValues).get();
345+
private void tagWrite(GatewayContext context, final List<TagPath> tagPaths, final List<Object> writeValues, SecurityContext securityContext, JSONObject result, String ipAddress, APITokenValidation validation) throws JSONException, ExecutionException, InterruptedException {
346+
List<QualityCode> writeResult = context.getTagManager().writeAsync(tagPaths, writeValues, securityContext).get();
320347

321348
if (tagPaths.size() == 1) {
322349
TagPath tagPath = tagPaths.get(0);

IgnitionNodeRED-gateway/src/main/resources/org/imdc/nodered/NodeRED.properties

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Noun=Node-RED API Token
44
Noun.Plural=Node-RED API Tokens
55
APITokens.MenuTitle=API Tokens
66
APITokens.NoRows=No Node-RED API tokens
7+
Settings=Settings
8+
Impersonate=Security Impersonation
79
Name.Name=Name
810
Name.Desc=Name of API token and secret pair
911
APIToken.Name=API Token
@@ -12,5 +14,11 @@ Secret.Name=Secret
1214
Secret.Desc=The secret (password) paired with the API token
1315
AuditProfile.Name=Audit Profile
1416
AuditProfile.Desc=The name of the audit profile that tag write actions will log to.
17+
SecurityLevels.Name=Security Levels
18+
SecurityLevels.Desc=A comma separated list of security levels to impersonate. If specified, security levels take precedence over roles and zones.
19+
Roles.Name=Roles
20+
Roles.Desc=A comma separated list of roles to impersonate.
21+
Zones.Name=Zones
22+
Zones.Desc=A comma separated list of zones to impersonate.
1523
Enabled.Name=Enabled
16-
Enabled.Desc=Whether or not the pair is enabled for Node-RED
24+
Enabled.Desc=Whether the token is enabled for Node-RED

0 commit comments

Comments
 (0)