Repository: node-red/node-red (js)

Issue:
3.1 environment credentials always return __PWRD__ placeholder
### Current Behavior

Upgraded from 3.0.2 to 3.1.0.

* Projects feature is enabled.
* `editorTheme.header.title` added with some info text
* `apiMaxLength: '32mb'`
* other than that default configuration with some extra nodes installed.
* Running nodered/node-red:latest Docker image.

### Tried to add environment credentials like this:
![image](https://github.com/node-red/node-red/assets/3417983/059f0be4-893e-4c27-a063-977754fe6798)

### And then use it like this:
![image](https://github.com/node-red/node-red/assets/3417983/17e0d450-9a5d-4e19-89c2-f86d3ade33b0)
```json
[{"id":"5fc48ad328d81f1b","type":"inject","z":"d005e303e68c6631","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"SOMETHING","payloadType":"env","x":790,"y":600,"wires":[["d38f2ec5e2d6f967"]]},{"id":"d38f2ec5e2d6f967","type":"debug","z":"d005e303e68c6631","name":"debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":950,"y":600,"wires":[]}]
```

This always results in string `__PWRD__` which AFAIK is placeholder for credentials, doesn't seem to be actually reading credentials at all but just returns placeholder.

### Expected Behavior

Env variable should return `qwerty` instead of `__PWRD__`.

### Steps To Reproduce

Route I took to hit this issue:
1. Have projects enabled on 3.0.2 (I'd assume using projects might affect outcome).
2. Upgrade to 3.1
3. Add some credentials to global environment.
4. Try to use those credentials.

### Example flow

_No response_

### Environment

- Node-RED version: v3.1.0
- Node.js version: v16.20.2 (comes with official Docker image)
- npm version: 8.19.4 (comes with official Docker image)
- Platform/OS: Docker nodered/node-red:latest on debian host.
- Browser: Chrome 118

Pull request:
Ensure global-config nodes lookup cred values properly
Fixes #4396

The way `global-config` nodes store their credentials is slightly different to regular nodes - they get stored an extra level deep under a `map` propertry.

Rather than pick apart the logic of why that's the case, the fix here is to lookup under that key.

Also adds a test for same.

Patch:
diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
index ce2dab9ef..2833341c6 100644
--- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
+++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js
@@ -161,7 +161,8 @@ class Flow {
             for (let i = 0; i < configNodes.length; i++) {
                 const node = this.flow.configs[configNodes[i]]
                 if (node.type === 'global-config' && node.env) {
-                    const nodeEnv = await flowUtil.evaluateEnvProperties(this, node.env, credentials.get(node.id))
+                    const globalCreds = credentials.get(node.id)?.map || {}
+                    const nodeEnv = await flowUtil.evaluateEnvProperties(this, node.env, globalCreds)
                     this._env = { ...this._env, ...nodeEnv }
                 }
             }


Test patch:
diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js
index 167691074..8406a01b7 100644
--- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js
+++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js
@@ -26,6 +26,7 @@ var flowUtils = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/util");
 var Flow = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Flow");
 var flows = NR_TEST_UTILS.require("@node-red/runtime/lib/flows");
 var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node");
+var credentials = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/credentials");
 var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks");
 var typeRegistry = NR_TEST_UTILS.require("@node-red/registry");
 
@@ -61,6 +62,7 @@ describe('Flow', function() {
         this.scope = n.scope;
         var node = this;
         this.foo = n.foo;
+        this.bar = n.bar;
         this.handled = 0;
         this.stopped = false;
         currentNodes[node.id] = node;
@@ -1235,11 +1237,12 @@ describe('Flow', function() {
     })
 
     describe("#env", function () {
+        afterEach(() => {
+            delete process.env.V0;
+            delete process.env.V1;
+            credentials.get.restore?.()
+        })
         it("can instantiate a node with environment variable property values of group and tab", async function () {
-            after(function() {
-                delete process.env.V0;
-                delete process.env.V1;
-            })
             process.env.V0 = "gv0";
             process.env.V1 = "gv1";
             process.env.V3 = "gv3";
@@ -1283,10 +1286,6 @@ describe('Flow', function() {
         });
 
         it("can access environment variable property using $parent", async function () {
-            after(function() {
-                delete process.env.V0;
-                delete process.env.V1;
-            })
             process.env.V0 = "gv0";
             process.env.V1 = "gv1";
             var config = flowUtils.parseConfig([
@@ -1321,9 +1320,6 @@ describe('Flow', function() {
         });
 
         it("can define environment variable using JSONata", async function () {
-            after(function() {
-                delete process.env.V0;
-            })
             var config = flowUtils.parseConfig([
                 {id:"t1",type:"tab",env:[
                     {"name": "V0", value: "1+2", type: "jsonata"}
@@ -1346,9 +1342,6 @@ describe('Flow', function() {
         });
 
         it("can access global environment variables defined as JSONata values", async function () {
-            after(function() {
-                delete process.env.V0;
-            })
             var config = flowUtils.parseConfig([
                 {id:"t1",type:"tab",env:[
                     {"name": "V0", value: "1+2", type: "jsonata"}
@@ -1370,15 +1363,21 @@ describe('Flow', function() {
             await flow.stop()
         });
         it("global flow can access global-config defined environment variables", async function () {
-            after(function() {
-                delete process.env.V0;
+            sinon.stub(credentials,"get").callsFake(function(id) {
+                if (id === 'gc') {
+                    return { map: { GC_CRED: 'gc_cred' }}
+                }
+                return null
             })
+
             const config = flowUtils.parseConfig([
                 {id:"gc", type:"global-config", env:[
-                    {"name": "GC0", value: "3+4", type: "jsonata"}
+                    {"name": "GC0", value: "3+4", type: "jsonata"},
+                    {"name": "GC_CRED", type: "cred"},
+                    
                 ]},
                 {id:"t1",type:"tab" },
-                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${GC0}",wires:[]},
+                {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${GC0}", bar:"${GC_CRED}", wires:[]},
             ]);
             // Two-arg call - makes this the global flow that handles global-config nodes
             const globalFlow = Flow.create({getSetting:v=>process.env[v]},config);
@@ -1390,6 +1389,7 @@ describe('Flow', function() {
 
             var activeNodes = flow.getActiveNodes();
             activeNodes["1"].foo.should.equal(7);
+            activeNodes["1"].bar.should.equal('gc_cred');
 
             await flow.stop()
             await globalFlow.stop()