Skip to content

Commit 7d4353e

Browse files
committed
Added support for Datasets and auditing tag writes
- Added support for reading Datasets from Ignition - Added audit profile to token settings to log tag writes - Fixed bug with false or 0 value when writing to a single tag
1 parent 1b923ce commit 7d4353e

7 files changed

Lines changed: 110 additions & 18 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.13.${timestamp}</moduleVersion>
50+
<moduleVersion>1.5.14.${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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.imdc.nodered;
22

3+
import com.inductiveautomation.ignition.gateway.audit.AuditProfileRecord;
34
import com.inductiveautomation.ignition.gateway.localdb.persistence.*;
45
import com.inductiveautomation.ignition.gateway.web.components.editors.PasswordEditorSource;
56
import org.apache.commons.lang3.StringUtils;
@@ -37,6 +38,11 @@ public void onValidate(SFieldMeta field, SRecordInstance instance) throws SExcep
3738
}
3839
});
3940
public static final EncodedStringField Secret = new EncodedStringField(META, "Secret", SFieldFlags.SMANDATORY);
41+
42+
public static final LongField AuditProfileId = new LongField(META, "AuditProfileId");
43+
public static final ReferenceField<AuditProfileRecord> AuditProfile =
44+
new ReferenceField<AuditProfileRecord>(META, AuditProfileRecord.META, "AuditProfile", AuditProfileId);
45+
4046
public static final BooleanField Enabled = new BooleanField(META, "Enabled").setDefault(true);
4147

4248
static {
@@ -47,6 +53,8 @@ public void onValidate(SFieldMeta field, SRecordInstance instance) throws SExcep
4753
Secret.getFormMeta().setFieldNameKey("NodeRED.Secret.Name");
4854
Secret.getFormMeta().setFieldDescriptionKey("NodeRED.Secret.Desc");
4955
Secret.getFormMeta().setEditorSource(PasswordEditorSource.getSharedInstance());
56+
AuditProfile.getFormMeta().setFieldNameKey("NodeRED.AuditProfile.Name");
57+
AuditProfile.getFormMeta().setFieldDescriptionKey("NodeRED.AuditProfile.Desc");
5058
Enabled.getFormMeta().setFieldNameKey("NodeRED.Enabled.Name");
5159
Enabled.getFormMeta().setFieldDescriptionKey("NodeRED.Enabled.Desc");
5260
}
@@ -59,4 +67,24 @@ public RecordMeta<?> getMeta() {
5967
public Long getId() {
6068
return getLong(Id);
6169
}
70+
71+
public Boolean isEnabled() {
72+
return getBoolean(Enabled);
73+
}
74+
75+
public String getName() {
76+
return getString(Name);
77+
}
78+
79+
public String getAPIToken() {
80+
return getString(APIToken);
81+
}
82+
83+
public String getSecret() {
84+
return getString(NodeREDAPITokens.Secret);
85+
}
86+
87+
public Long getAuditProfileId() {
88+
return getLong(AuditProfileId);
89+
}
6290
}

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

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

3+
import com.inductiveautomation.ignition.gateway.audit.AuditProfileRecord;
34
import com.inductiveautomation.ignition.gateway.model.GatewayContext;
45
import org.imdc.nodered.NodeREDAPITokens;
56
import simpleorm.dataset.SQuery;
67

78
public class APITokenValidation {
89
private boolean success;
9-
private String errorMessage;
10+
private String errorMessage, tokenName, token, auditProfileName;
1011

11-
public APITokenValidation(boolean success, String errorMessage) {
12+
public APITokenValidation(boolean success, String errorMessage, String tokenName, String token, String auditProfileName) {
1213
this.success = success;
1314
this.errorMessage = errorMessage;
15+
this.tokenName = tokenName;
16+
this.token = token;
17+
this.auditProfileName = auditProfileName;
1418
}
1519

1620
public boolean isSuccess() {
@@ -29,6 +33,22 @@ public void setErrorMessage(String errorMessage) {
2933
this.errorMessage = errorMessage;
3034
}
3135

36+
public String getTokenName() {
37+
return tokenName;
38+
}
39+
40+
public String getToken() {
41+
return token;
42+
}
43+
44+
public String getAuditProfileName() {
45+
return auditProfileName;
46+
}
47+
48+
public boolean isAuditProfileDefined() {
49+
return auditProfileName != null;
50+
}
51+
3252
public static APITokenValidation validateToken(GatewayContext context, String apiToken, String secret) {
3353
boolean success = true;
3454
String errorMessage = null;
@@ -40,15 +60,25 @@ public static APITokenValidation validateToken(GatewayContext context, String ap
4060
success = false;
4161
errorMessage = "Invalid API token and secret";
4262
} else {
43-
if (!r.getBoolean(NodeREDAPITokens.Enabled)) {
63+
if (!r.isEnabled()) {
4464
success = false;
4565
errorMessage = "API token and secret is disabled";
46-
} else if (!r.getString(NodeREDAPITokens.Secret).equals(secret)) {
66+
} else if (!r.getSecret().equals(secret)) {
4767
success = false;
4868
errorMessage = "Invalid API token and secret";
4969
}
5070
}
5171

52-
return new APITokenValidation(success, errorMessage);
72+
String auditProfileName = null;
73+
if (r.getAuditProfileId() != null) {
74+
SQuery<AuditProfileRecord> auditQuery = new SQuery<>(AuditProfileRecord.META);
75+
auditQuery.eq(AuditProfileRecord.Id, r.getAuditProfileId());
76+
AuditProfileRecord auditRecord = context.getPersistenceInterface().queryOne(auditQuery);
77+
if (auditRecord != null) {
78+
auditProfileName = auditRecord.getName();
79+
}
80+
}
81+
82+
return new APITokenValidation(success, errorMessage, r.getName(), r.getAPIToken(), auditProfileName);
5383
}
5484
}

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

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

3+
import com.inductiveautomation.ignition.common.Dataset;
4+
import com.inductiveautomation.ignition.common.TypeUtilities;
35
import com.inductiveautomation.ignition.common.browsing.BrowseFilter;
46
import com.inductiveautomation.ignition.common.browsing.Results;
57
import com.inductiveautomation.ignition.common.model.values.QualifiedValue;
68
import com.inductiveautomation.ignition.common.model.values.QualityCode;
9+
import com.inductiveautomation.ignition.common.script.builtin.DatasetUtilities;
710
import com.inductiveautomation.ignition.common.tags.browsing.NodeDescription;
811
import com.inductiveautomation.ignition.common.tags.model.TagPath;
912
import com.inductiveautomation.ignition.common.tags.paths.parser.TagPathParser;
13+
import com.inductiveautomation.ignition.common.util.AuditStatus;
14+
import com.inductiveautomation.ignition.gateway.audit.AuditContext;
15+
import com.inductiveautomation.ignition.gateway.audit.AuditProfile;
16+
import com.inductiveautomation.ignition.gateway.audit.AuditRecord;
1017
import com.inductiveautomation.ignition.gateway.model.GatewayContext;
1118
import org.json.JSONArray;
1219
import org.json.JSONException;
@@ -21,10 +28,7 @@
2128
import java.io.BufferedReader;
2229
import java.io.IOException;
2330
import java.text.SimpleDateFormat;
24-
import java.util.ArrayList;
25-
import java.util.Collection;
26-
import java.util.Iterator;
27-
import java.util.List;
31+
import java.util.*;
2832
import java.util.concurrent.ExecutionException;
2933

3034
public class NodeREDServlet extends HttpServlet {
@@ -69,6 +73,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
6973
String contentType = req.getContentType();
7074
JSONObject input = null;
7175
JSONObject ret = new JSONObject();
76+
String ipAddress = req.getRemoteAddr();
7277

7378
if ("application/json".equals(contentType)) {
7479
String jsonString = reqToJSON(req);
@@ -157,7 +162,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
157162
if (values.size() != tagPaths.size()) {
158163
errorMessage = "Number of tag paths (" + tagPaths.size() + ") does not match values (" + values.size() + ")";
159164
} else {
160-
tagWrite(context, tagPaths, values, result);
165+
tagWrite(context, tagPaths, values, result, ipAddress, validation);
161166
}
162167
}
163168
}
@@ -203,7 +208,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
203208
public static void setTagValue(List<TagPath> tagPaths, List<QualifiedValue> tagValues, JSONObject result) throws JSONException {
204209
if (tagPaths.size() == 1) {
205210
result.put("tagPath", tagPaths.get(0).toStringFull());
206-
result.put("value", tagValues.get(0).getValue());
211+
result.put("value", checkObject(tagValues.get(0).getValue()));
207212
setQuality(result, tagValues.get(0).getQuality());
208213
result.put("timestamp", DF.format(tagValues.get(0).getTimestamp()));
209214
} else {
@@ -213,7 +218,7 @@ public static void setTagValue(List<TagPath> tagPaths, List<QualifiedValue> tagV
213218
QualifiedValue tagValue = tagValues.get(i);
214219
JSONObject tagObject = new JSONObject();
215220
tagObject.put("tagPath", tagPath.toStringFull());
216-
tagObject.put("value", tagValue.getValue());
221+
tagObject.put("value", checkObject(tagValue.getValue()));
217222
setQuality(tagObject, tagValue.getQuality());
218223
tagObject.put("timestamp", DF.format(tagValue.getTimestamp()));
219224
resultValues.put(tagObject);
@@ -222,6 +227,14 @@ public static void setTagValue(List<TagPath> tagPaths, List<QualifiedValue> tagV
222227
}
223228
}
224229

230+
public static Object checkObject(Object value) {
231+
if (value instanceof Dataset) {
232+
return DatasetUtilities.toJSONObject((Dataset) value);
233+
}
234+
235+
return value;
236+
}
237+
225238
public static void setQuality(JSONObject result, QualityCode qual) throws JSONException {
226239
JSONObject quality = new JSONObject();
227240
quality.put("name", qual.getName());
@@ -273,29 +286,48 @@ private void tagRead(GatewayContext context, final List<TagPath> tagPaths, JSONO
273286
setTagValue(tagPaths, tagValues, result);
274287
}
275288

276-
private void tagWrite(GatewayContext context, final List<TagPath> tagPaths, final List<Object> writeValues, JSONObject result) throws JSONException, ExecutionException, InterruptedException {
289+
private void tagWrite(GatewayContext context, final List<TagPath> tagPaths, final List<Object> writeValues, JSONObject result, String ipAddress, APITokenValidation validation) throws JSONException, ExecutionException, InterruptedException {
277290
List<QualityCode> writeResult = context.getTagManager().writeAsync(tagPaths, writeValues).get();
291+
278292
if (tagPaths.size() == 1) {
279293
TagPath tagPath = tagPaths.get(0);
280294
QualityCode quality = writeResult.get(0);
295+
Object value = writeValues.get(0);
281296
result.put("tagPath", tagPath.toStringFull());
282-
result.put("value", writeValues.get(0));
297+
result.put("value", value);
283298
setQuality(result, quality);
299+
audit(context, ipAddress, validation, tagPath, value, quality);
284300
} else {
285301
JSONArray resultValues = new JSONArray();
286302
for (int i = 0; i < tagPaths.size(); i++) {
287303
JSONObject tagObject = new JSONObject();
288304
TagPath tagPath = tagPaths.get(i);
289305
QualityCode quality = writeResult.get(i);
306+
Object value = writeValues.get(i);
290307
tagObject.put("tagPath", tagPath.toStringFull());
291-
tagObject.put("value", writeValues.get(i));
308+
tagObject.put("value", value);
292309
setQuality(tagObject, quality);
293310
resultValues.put(tagObject);
311+
audit(context, ipAddress, validation, tagPath, value, quality);
294312
}
295313
result.put("values", resultValues);
296314
}
297315
}
298316

317+
private void audit(GatewayContext context, String ipAddress, APITokenValidation validation, TagPath tagPath, Object writeValue, QualityCode writeResult) {
318+
if (validation.isAuditProfileDefined()) {
319+
try {
320+
AuditProfile auditProfile = context.getAuditManager().getProfile(validation.getAuditProfileName());
321+
AuditContext auditContext = context.getAuditManager().getAuditContext().orElseGet(AuditContext.UNKNOWN);
322+
String qualifiedPath = auditContext.getPath().extend("NodeRED", validation.getTokenName()).toString();
323+
AuditRecord auditRecord = auditContext.toRecordBuilder().setAction("tag write").setActionTarget(tagPath.toStringFull()).setActionValue(TypeUtilities.toString(writeValue)).setTimestamp(new Date()).setActor(validation.getToken()).setActorHost(ipAddress).setOriginatingSystem(qualifiedPath).setStatusCode(writeResult.isGood() ? AuditStatus.GOOD.getRawValue() : AuditStatus.BAD.getRawValue()).build();
324+
auditProfile.audit(auditRecord);
325+
} catch (Throwable t) {
326+
logger.error("Error with audit: " + validation.getAuditProfileName(), t);
327+
}
328+
}
329+
}
330+
299331
private String reqToJSON(HttpServletRequest req) throws IOException {
300332
StringBuilder sb = new StringBuilder();
301333
String line;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ APIToken.Name=API Token
1010
APIToken.Desc=16 character alphanumeric API token
1111
Secret.Name=Secret
1212
Secret.Desc=The secret (password) paired with the API token
13+
AuditProfile.Name=Audit Profile
14+
AuditProfile.Desc=The name of the audit profile that tag write actions will log to.
1315
Enabled.Name=Enabled
1416
Enabled.Desc=Whether or not the pair is enabled for Node-RED

node-red-contrib-ignition/node-handlers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ var IgnitionNodesImpl = (function () {
9595
var tagValues = [];
9696

9797
if(!values){
98-
var tagValue = msg.payload.tagValue ? msg.payload.tagValue : this.config.tagValue;
98+
var tagValue = !(typeof msg.payload.tagValue === 'undefined' || msg.payload.tagValue === null) ? msg.payload.tagValue : this.config.tagValue;
9999

100100
if(typeof tagValue === 'undefined' || tagValue === null){
101101
errorHandling.HandleResponse(RED, this.node, this.config, msg, result, 2, "Invalid tag value", "No tag value supplied");

node-red-contrib-ignition/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-red-contrib-ignition-nodes",
3-
"version": "1.5.13",
3+
"version": "1.5.14",
44
"description": "Adds support for reading, writing, and browsing tags in Ignition",
55
"dependencies": {
66
"ws": "^7.5.3",

0 commit comments

Comments
 (0)