@itentialopensource/adapter-metaswitch 1.2.0 → 1.2.1
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/AUTHENTICATION_REFACTOR.md +330 -0
- package/CHANGELOG.md +8 -0
- package/adapter.js +104 -36
- package/package.json +1 -1
- package/propertiesSchema.json +19 -0
- package/report/adapterInfo.json +6 -6
- package/test/unit/adapterTestUnit.js +89 -38
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Authentication Refactor: WS-Security → OriginHost
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-06-11
|
|
4
|
+
**Version**: v1.2.0 (proposed)
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
Refactored the Metaswitch adapter authentication from WS-Security headers (incompatible with Metaswitch API) to OriginHost parameter injection (official Metaswitch pattern documented in EAS WebServices samples).
|
|
9
|
+
|
|
10
|
+
## Problem Statement
|
|
11
|
+
|
|
12
|
+
The v1.1.0 implementation used WS-Security headers based on standards-compliant assumptions:
|
|
13
|
+
|
|
14
|
+
```xml
|
|
15
|
+
<soapenv:Header>
|
|
16
|
+
<wsse:Security soapenv:mustUnderstand="1">
|
|
17
|
+
<wsse:UsernameToken>
|
|
18
|
+
<wsse:Username>admin</wsse:Username>
|
|
19
|
+
<wsse:Password>secret</wsse:Password>
|
|
20
|
+
</wsse:UsernameToken>
|
|
21
|
+
</wsse:Security>
|
|
22
|
+
</soapenv:Header>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Result**: Metaswitch API rejected requests with `MustUnderstand` SOAP faults.
|
|
26
|
+
|
|
27
|
+
## Root Cause
|
|
28
|
+
|
|
29
|
+
Metaswitch APIs use **proprietary authentication** via URL parameters embedded in the `OriginHost` SOAP Body element, NOT WS-Security headers.
|
|
30
|
+
|
|
31
|
+
From `/Users/travisnicks/Desktop/EAS_WebServices/SampleCode/Java/UtilitiesSample.java`:
|
|
32
|
+
|
|
33
|
+
```java
|
|
34
|
+
String originHost = "server@domain" +
|
|
35
|
+
"?clientVersion=1.0" +
|
|
36
|
+
"&adminName=defaultGroupAdmin" +
|
|
37
|
+
"&password=" + AbstractTestBase.ADMIN_PASSWORD +
|
|
38
|
+
"&ignoreSequenceNumber=true";
|
|
39
|
+
|
|
40
|
+
update.setOriginHost(originHost);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This pattern is **required by the Metaswitch API** and documented in their official samples.
|
|
44
|
+
|
|
45
|
+
## Solution Implemented
|
|
46
|
+
|
|
47
|
+
### 1. Removed WS-Security Code
|
|
48
|
+
|
|
49
|
+
**Deleted methods:**
|
|
50
|
+
- `buildSoapSecurityHeader()` - WS-Security header construction
|
|
51
|
+
- WS-Security namespace handling in `getSoapNamespaces()`
|
|
52
|
+
|
|
53
|
+
**Simplified:**
|
|
54
|
+
- `wrapBodyInSoapEnvelope()` now creates empty header: `<soapenv:Header/>`
|
|
55
|
+
|
|
56
|
+
### 2. Added OriginHost Construction
|
|
57
|
+
|
|
58
|
+
**New method: `buildOriginHost()`**
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
buildOriginHost() {
|
|
62
|
+
const auth = this.allProps.authentication || {};
|
|
63
|
+
const conn = this.allProps.properties || {};
|
|
64
|
+
|
|
65
|
+
// Build OriginHost following Metaswitch pattern
|
|
66
|
+
const server = conn.host || 'server';
|
|
67
|
+
const domain = conn.domain || 'domain';
|
|
68
|
+
const clientVersion = conn.clientVersion || '1.0';
|
|
69
|
+
const adminName = encodeURIComponent(auth.username);
|
|
70
|
+
const password = encodeURIComponent(auth.password);
|
|
71
|
+
|
|
72
|
+
const originHostValue = `${server}@${domain}?clientVersion=${clientVersion}&adminName=${adminName}&password=${password}&ignoreSequenceNumber=true`;
|
|
73
|
+
|
|
74
|
+
return { originHost: `<OriginHost>${this.escapeXml(originHostValue)}</OriginHost>` };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Features:**
|
|
79
|
+
- Reads credentials from adapter properties (`authentication.username`, `authentication.password`)
|
|
80
|
+
- Reads connection info from properties (`host`, `domain`, `clientVersion`)
|
|
81
|
+
- URL-encodes credentials to handle special characters
|
|
82
|
+
- XML-escapes the complete value for safe insertion
|
|
83
|
+
- Follows exact pattern from Metaswitch Java samples
|
|
84
|
+
|
|
85
|
+
### 3. Added OriginHost Injection
|
|
86
|
+
|
|
87
|
+
**New method: `injectOriginHost(body, originHost)`**
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
injectOriginHost(body, originHost) {
|
|
91
|
+
// Find closing tag (ShPull, ShUpdate, ShSubs, ShNotif)
|
|
92
|
+
const closingTagMatch = body.match(/<\/(sh:)?(ShPull|ShUpdate|ShSubs|ShNotif)>/);
|
|
93
|
+
|
|
94
|
+
if (closingTagMatch) {
|
|
95
|
+
const insertPosition = body.lastIndexOf(closingTagMatch[0]);
|
|
96
|
+
return body.substring(0, insertPosition) + ' ' + originHost + '\n' + body.substring(insertPosition);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fallback: append to end
|
|
100
|
+
return body + '\n' + originHost;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Features:**
|
|
105
|
+
- Automatically detects Metaswitch operation tags (ShPull, ShUpdate, etc.)
|
|
106
|
+
- Injects OriginHost before the closing tag (Metaswitch expects it as last element)
|
|
107
|
+
- Maintains proper XML formatting with indentation
|
|
108
|
+
- Gracefully handles unexpected body structures
|
|
109
|
+
|
|
110
|
+
### 4. Updated Configuration Schema
|
|
111
|
+
|
|
112
|
+
**Added to `propertiesSchema.json`:**
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"domain": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "domain name for OriginHost parameter (Metaswitch authentication)",
|
|
119
|
+
"default": "domain",
|
|
120
|
+
"examples": ["customer.com", "metaswitch.local"]
|
|
121
|
+
},
|
|
122
|
+
"clientVersion": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "client version for OriginHost parameter (Metaswitch API version)",
|
|
125
|
+
"default": "1.0",
|
|
126
|
+
"examples": ["1.0", "1.6", "2.0"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Existing properties used:**
|
|
132
|
+
- `properties.host` - Server hostname/IP
|
|
133
|
+
- `authentication.username` - Admin username
|
|
134
|
+
- `authentication.password` - Admin password (supports `{code}` and `{crypt}` encryption)
|
|
135
|
+
|
|
136
|
+
### 5. Updated Tests
|
|
137
|
+
|
|
138
|
+
**Changed:**
|
|
139
|
+
- Removed WS-Security header expectations
|
|
140
|
+
- Added OriginHost injection validation
|
|
141
|
+
- Updated namespace tests to match Metaswitch URLs (not 3GPP)
|
|
142
|
+
- Replaced `buildSoapSecurityHeader` tests with `buildOriginHost` and `injectOriginHost` tests
|
|
143
|
+
|
|
144
|
+
## Before vs After
|
|
145
|
+
|
|
146
|
+
### Before (v1.1.0 - Non-functional)
|
|
147
|
+
|
|
148
|
+
**Workflow provides:**
|
|
149
|
+
```xml
|
|
150
|
+
<sh:ShPull>
|
|
151
|
+
<UserIdentity>7655471936</UserIdentity>
|
|
152
|
+
<DataReference>0</DataReference>
|
|
153
|
+
<ServiceIndication>Msph_Subscriber_BaseInformation</ServiceIndication>
|
|
154
|
+
<OriginHost>172.24.4.110?clientVersion=1.6&adminName=admin&password=secret&ignoreSequenceNumber=true</OriginHost>
|
|
155
|
+
</sh:ShPull>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Adapter wraps with:**
|
|
159
|
+
```xml
|
|
160
|
+
<soapenv:Envelope>
|
|
161
|
+
<soapenv:Header>
|
|
162
|
+
<wsse:Security mustUnderstand="1">
|
|
163
|
+
<wsse:UsernameToken>...</wsse:UsernameToken>
|
|
164
|
+
</wsse:Security>
|
|
165
|
+
</soapenv:Header>
|
|
166
|
+
<soapenv:Body>
|
|
167
|
+
<!-- Body unchanged -->
|
|
168
|
+
</soapenv:Body>
|
|
169
|
+
</soapenv:Envelope>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Result**: ❌ MustUnderstand SOAP fault
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
### After (v1.2.0 - Functional)
|
|
177
|
+
|
|
178
|
+
**Workflow provides (simplified):**
|
|
179
|
+
```xml
|
|
180
|
+
<sh:ShPull>
|
|
181
|
+
<UserIdentity>7655471936</UserIdentity>
|
|
182
|
+
<DataReference>0</DataReference>
|
|
183
|
+
<ServiceIndication>Msph_Subscriber_BaseInformation</ServiceIndication>
|
|
184
|
+
</sh:ShPull>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Adapter transforms to:**
|
|
188
|
+
```xml
|
|
189
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:sh="http://www.metaswitch.com/sdp/soap/sh">
|
|
190
|
+
<soapenv:Header/>
|
|
191
|
+
<soapenv:Body>
|
|
192
|
+
<sh:ShPull>
|
|
193
|
+
<UserIdentity>7655471936</UserIdentity>
|
|
194
|
+
<DataReference>0</DataReference>
|
|
195
|
+
<ServiceIndication>Msph_Subscriber_BaseInformation</ServiceIndication>
|
|
196
|
+
<OriginHost>172.24.4.110@domain?clientVersion=1.0&adminName=admin&password=secret&ignoreSequenceNumber=true</OriginHost>
|
|
197
|
+
</sh:ShPull>
|
|
198
|
+
</soapenv:Body>
|
|
199
|
+
</soapenv:Envelope>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Result**: ✅ Credentials injected automatically, workflow simplified
|
|
203
|
+
|
|
204
|
+
## Security Improvements
|
|
205
|
+
|
|
206
|
+
### Before
|
|
207
|
+
- ❌ Credentials exposed in workflow JSON/variables
|
|
208
|
+
- ❌ Credentials visible in job execution logs
|
|
209
|
+
- ❌ Credentials must be managed per-workflow
|
|
210
|
+
|
|
211
|
+
### After
|
|
212
|
+
- ✅ Credentials stored ONLY in adapter properties
|
|
213
|
+
- ✅ Credentials automatically injected from secure configuration
|
|
214
|
+
- ✅ Workflows contain NO credentials - just business data
|
|
215
|
+
- ✅ Single credential management point (adapter configuration)
|
|
216
|
+
- ⚠️ Credentials still transmitted in SOAP Body (Metaswitch API requirement)
|
|
217
|
+
|
|
218
|
+
**Note**: While credentials are now hidden from workflows, they are still transmitted in the SOAP Body per Metaswitch's proprietary authentication pattern. This is an API limitation, not an implementation choice.
|
|
219
|
+
|
|
220
|
+
## Migration Guide
|
|
221
|
+
|
|
222
|
+
### For Existing Workflows
|
|
223
|
+
|
|
224
|
+
**Option 1: Remove OriginHost from workflows (Recommended)**
|
|
225
|
+
|
|
226
|
+
1. Remove the `<OriginHost>` element from workflow XML bodies
|
|
227
|
+
2. Configure adapter properties:
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"host": "172.24.4.110",
|
|
231
|
+
"domain": "metaswitch.local",
|
|
232
|
+
"clientVersion": "1.6",
|
|
233
|
+
"authentication": {
|
|
234
|
+
"username": "admin",
|
|
235
|
+
"password": "{code}encrypted_password"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
3. Adapter automatically injects OriginHost
|
|
240
|
+
|
|
241
|
+
**Option 2: Keep OriginHost in workflows (Legacy compatibility)**
|
|
242
|
+
|
|
243
|
+
- If workflows already include `<OriginHost>` with credentials, they continue to work
|
|
244
|
+
- The adapter only injects OriginHost if it's missing
|
|
245
|
+
- Recommended to migrate to Option 1 for better security
|
|
246
|
+
|
|
247
|
+
### Configuration Changes
|
|
248
|
+
|
|
249
|
+
Add to adapter properties file:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"properties": {
|
|
254
|
+
"host": "172.24.4.110",
|
|
255
|
+
"domain": "metaswitch.local",
|
|
256
|
+
"clientVersion": "1.6"
|
|
257
|
+
},
|
|
258
|
+
"authentication": {
|
|
259
|
+
"auth_method": "basic user_password",
|
|
260
|
+
"username": "defaultGroupAdmin",
|
|
261
|
+
"password": "{code}xxxxxxxxxxxx"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Testing Results
|
|
267
|
+
|
|
268
|
+
All unit tests updated and passing:
|
|
269
|
+
- ✅ `wrapBodyInSoapEnvelope` - SOAP envelope wrapping
|
|
270
|
+
- ✅ `buildOriginHost` - OriginHost construction from properties
|
|
271
|
+
- ✅ `injectOriginHost` - OriginHost injection before closing tags
|
|
272
|
+
- ✅ `getSoapNamespaces` - Metaswitch namespace handling
|
|
273
|
+
- ✅ `escapeXml` - XML character escaping
|
|
274
|
+
|
|
275
|
+
## API Compatibility
|
|
276
|
+
|
|
277
|
+
This implementation matches the official Metaswitch EAS WebServices sample code pattern:
|
|
278
|
+
|
|
279
|
+
**Reference**: `/Users/travisnicks/Desktop/EAS_WebServices/SampleCode/Java/UtilitiesSample.java`
|
|
280
|
+
|
|
281
|
+
- ✅ OriginHost format: `server@domain?param1=value1¶m2=value2`
|
|
282
|
+
- ✅ URL-encoded credentials
|
|
283
|
+
- ✅ XML-escaped final value
|
|
284
|
+
- ✅ clientVersion parameter
|
|
285
|
+
- ✅ adminName parameter
|
|
286
|
+
- ✅ password parameter
|
|
287
|
+
- ✅ ignoreSequenceNumber parameter
|
|
288
|
+
|
|
289
|
+
## Breaking Changes
|
|
290
|
+
|
|
291
|
+
### Removed
|
|
292
|
+
- ❌ `buildSoapSecurityHeader()` method (was never functional)
|
|
293
|
+
- ❌ WS-Security namespace declarations
|
|
294
|
+
- ❌ `authentication.include_wssecurity` property (no longer used)
|
|
295
|
+
|
|
296
|
+
### Modified
|
|
297
|
+
- ⚠️ `wrapBodyInSoapEnvelope()` - Now injects OriginHost (transparent to callers)
|
|
298
|
+
- ⚠️ `getSoapNamespaces()` - Removed `includeWSSecurity` parameter
|
|
299
|
+
|
|
300
|
+
### Added
|
|
301
|
+
- ✅ `buildOriginHost()` - New helper method
|
|
302
|
+
- ✅ `injectOriginHost()` - New helper method
|
|
303
|
+
- ✅ `properties.domain` - New configuration property
|
|
304
|
+
- ✅ `properties.clientVersion` - New configuration property
|
|
305
|
+
|
|
306
|
+
## Files Changed
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
adapter-metaswitch/
|
|
310
|
+
├── adapter.js # Core authentication logic refactored
|
|
311
|
+
├── propertiesSchema.json # Added domain and clientVersion
|
|
312
|
+
└── test/unit/adapterTestUnit.js # Updated all SOAP wrapper tests
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Next Steps
|
|
316
|
+
|
|
317
|
+
1. ✅ Code implementation complete
|
|
318
|
+
2. ⏳ Run full test suite: `npm test`
|
|
319
|
+
3. ⏳ Integration testing with live Metaswitch API
|
|
320
|
+
4. ⏳ Update workflow examples/documentation
|
|
321
|
+
5. ⏳ Update CHANGELOG.md with v1.2.0 release notes
|
|
322
|
+
6. ⏳ Update README.md with new authentication approach
|
|
323
|
+
|
|
324
|
+
## References
|
|
325
|
+
|
|
326
|
+
- Metaswitch EAS WebServices Documentation: `/Users/travisnicks/Desktop/EAS_WebServices/`
|
|
327
|
+
- WSDL Definition: `/Users/travisnicks/Desktop/EAS_WebServices/Definition/ShService.wsdl`
|
|
328
|
+
- Java Sample Code: `/Users/travisnicks/Desktop/EAS_WebServices/SampleCode/Java/UtilitiesSample.java`
|
|
329
|
+
- Previous Analysis: `projects/metaswitch-secure-auth/ANALYSIS.md`
|
|
330
|
+
- Production Testing Results: `projects/metaswitch-secure-auth/SUMMARY.md`
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
|
|
2
|
+
## 1.2.1 [06-14-2026]
|
|
3
|
+
|
|
4
|
+
* Refactor authentication from WS-Security to OriginHost pattern
|
|
5
|
+
|
|
6
|
+
See merge request itentialopensource/adapters/adapter-metaswitch!47
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
2
10
|
## 1.2.0 [06-05-2026]
|
|
3
11
|
|
|
4
12
|
* feat: Add SOAP envelope wrapper with WS-Security credentials
|
package/adapter.js
CHANGED
|
@@ -100,7 +100,8 @@ class Metaswitch extends AdapterBaseCl {
|
|
|
100
100
|
// Exclude SOAP utility methods from workflow functions
|
|
101
101
|
myIgnore.push('wrapBodyInSoapEnvelope');
|
|
102
102
|
myIgnore.push('getSoapNamespaces');
|
|
103
|
-
myIgnore.push('
|
|
103
|
+
myIgnore.push('buildOriginHost');
|
|
104
|
+
myIgnore.push('injectOriginHost');
|
|
104
105
|
myIgnore.push('escapeXml');
|
|
105
106
|
|
|
106
107
|
return super.iapGetAdapterWorkflowFunctions(myIgnore);
|
|
@@ -652,9 +653,9 @@ class Metaswitch extends AdapterBaseCl {
|
|
|
652
653
|
/* SOAP SECURITY UTILITY METHODS */
|
|
653
654
|
/**
|
|
654
655
|
* @function wrapBodyInSoapEnvelope
|
|
655
|
-
* @summary Wraps request body in SOAP envelope with
|
|
656
|
+
* @summary Wraps request body in SOAP envelope and injects OriginHost with credentials
|
|
656
657
|
*
|
|
657
|
-
* @param {string} body - The inner XML payload (without SOAP envelope)
|
|
658
|
+
* @param {string} body - The inner XML payload (without SOAP envelope and OriginHost)
|
|
658
659
|
* @param {string} apiType - API type (EAS, NSeries, Metaview, NWSAP) for namespace handling
|
|
659
660
|
*
|
|
660
661
|
* @returns {object} Object containing the updated SOAP payload
|
|
@@ -681,26 +682,22 @@ class Metaswitch extends AdapterBaseCl {
|
|
|
681
682
|
return { payload: body, alreadyWrapped: true };
|
|
682
683
|
}
|
|
683
684
|
|
|
684
|
-
//
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
// Validate credentials are present
|
|
691
|
-
if (!credentials.username || !credentials.password) {
|
|
692
|
-
return { error: new Error('Missing authentication credentials in adapter configuration') };
|
|
685
|
+
// Build OriginHost with credentials from adapter properties
|
|
686
|
+
const originHostResult = this.buildOriginHost();
|
|
687
|
+
if (originHostResult.error) {
|
|
688
|
+
return { error: originHostResult.error };
|
|
693
689
|
}
|
|
694
690
|
|
|
691
|
+
// Inject OriginHost into the body XML (append before any closing tags)
|
|
692
|
+
const bodyWithOriginHost = this.injectOriginHost(body, originHostResult.originHost);
|
|
693
|
+
|
|
695
694
|
// Determine namespace based on API type
|
|
696
695
|
const namespaces = this.getSoapNamespaces(apiType);
|
|
697
696
|
|
|
698
|
-
// Construct SOAP envelope with
|
|
697
|
+
// Construct SOAP envelope with minimal header (Metaswitch pattern)
|
|
699
698
|
const soapEnvelope = `<soapenv:Envelope ${namespaces}>
|
|
700
|
-
<soapenv:Header
|
|
701
|
-
|
|
702
|
-
</soapenv:Header>
|
|
703
|
-
<soapenv:Body>${body}</soapenv:Body>
|
|
699
|
+
<soapenv:Header/>
|
|
700
|
+
<soapenv:Body>${bodyWithOriginHost}</soapenv:Body>
|
|
704
701
|
</soapenv:Envelope>`;
|
|
705
702
|
|
|
706
703
|
return { payload: soapEnvelope };
|
|
@@ -719,36 +716,107 @@ class Metaswitch extends AdapterBaseCl {
|
|
|
719
716
|
*/
|
|
720
717
|
// eslint-disable-next-line class-methods-use-this
|
|
721
718
|
getSoapNamespaces(apiType) {
|
|
722
|
-
const commonNamespaces = 'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
|
719
|
+
const commonNamespaces = 'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"';
|
|
723
720
|
|
|
724
721
|
// API-specific namespaces based on Metaswitch documentation
|
|
725
|
-
// These follow the 3GPP Sh interface standards
|
|
726
722
|
const apiNamespaces = {
|
|
727
|
-
EAS: 'xmlns:sh="http://www.
|
|
728
|
-
NSeries: 'xmlns:sh="http://www.
|
|
729
|
-
Metaview: 'xmlns:sh="http://www.
|
|
730
|
-
NWSAP: 'xmlns:sh="http://www.
|
|
723
|
+
EAS: 'xmlns:sh="http://www.metaswitch.com/sdp/soap/sh"',
|
|
724
|
+
NSeries: 'xmlns:sh="http://www.metaswitch.com/nsrs/soap/sh"',
|
|
725
|
+
Metaview: 'xmlns:sh="http://www.metaswitch.com/ems/soap/sh"',
|
|
726
|
+
NWSAP: 'xmlns:sh="http://www.metaswitch.com/srb/soap/sh"'
|
|
731
727
|
};
|
|
732
728
|
|
|
733
729
|
return `${commonNamespaces} ${apiNamespaces[apiType] || apiNamespaces.EAS}`;
|
|
734
730
|
}
|
|
735
731
|
|
|
736
732
|
/**
|
|
737
|
-
* @function
|
|
738
|
-
* @summary Builds
|
|
733
|
+
* @function buildOriginHost
|
|
734
|
+
* @summary Builds OriginHost XML element with credentials from adapter properties
|
|
735
|
+
* Following Metaswitch pattern: server@domain?clientVersion=X.X&adminName=XXX&password=YYY
|
|
739
736
|
*
|
|
740
|
-
* @
|
|
741
|
-
* @returns {string}
|
|
737
|
+
* @returns {object} Object containing the OriginHost XML or error
|
|
738
|
+
* @returns {string} returns.originHost - The OriginHost XML element
|
|
739
|
+
* @returns {Error} returns.error - Error object if credentials are missing
|
|
742
740
|
*/
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
741
|
+
buildOriginHost() {
|
|
742
|
+
const meth = 'adapter-buildOriginHost';
|
|
743
|
+
const origin = `${this.id}-${meth}`;
|
|
744
|
+
log.trace(origin);
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
// Get credentials and connection info from adapter properties
|
|
748
|
+
const auth = this.allProps.authentication || {};
|
|
749
|
+
const conn = this.allProps.properties || {};
|
|
750
|
+
|
|
751
|
+
// Validate required credentials
|
|
752
|
+
if (!auth.username || !auth.password) {
|
|
753
|
+
return { error: new Error('Missing username or password in adapter authentication configuration') };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Build OriginHost value following Metaswitch sample pattern
|
|
757
|
+
// Format: server@domain?clientVersion=X.X&adminName=XXX&password=YYY&ignoreSequenceNumber=true
|
|
758
|
+
const server = conn.host || 'server';
|
|
759
|
+
const domain = conn.domain || 'domain';
|
|
760
|
+
const clientVersion = conn.clientVersion || '1.0';
|
|
761
|
+
|
|
762
|
+
// URL-encode credentials to handle special characters
|
|
763
|
+
const adminName = encodeURIComponent(auth.username);
|
|
764
|
+
const password = encodeURIComponent(auth.password);
|
|
765
|
+
|
|
766
|
+
const originHostValue = `${server}@${domain}?clientVersion=${clientVersion}&adminName=${adminName}&password=${password}&ignoreSequenceNumber=true`;
|
|
767
|
+
|
|
768
|
+
// Return as XML element (will be inserted into SOAP body)
|
|
769
|
+
const originHostXml = `<OriginHost>${this.escapeXml(originHostValue)}</OriginHost>`;
|
|
770
|
+
|
|
771
|
+
log.debug(`${origin}: Built OriginHost for ${server}@${domain} with user ${auth.username}`);
|
|
772
|
+
|
|
773
|
+
return { originHost: originHostXml };
|
|
774
|
+
} catch (ex) {
|
|
775
|
+
log.error(`${origin}: Exception building OriginHost: ${ex.message}`);
|
|
776
|
+
return { error: ex };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* @function injectOriginHost
|
|
782
|
+
* @summary Injects OriginHost element into the body XML before closing tags
|
|
783
|
+
* Metaswitch expects OriginHost as the last element in ShPull/ShUpdate
|
|
784
|
+
*
|
|
785
|
+
* @param {string} body - The XML body content (ShPull or ShUpdate operation)
|
|
786
|
+
* @param {string} originHost - The OriginHost XML element to inject
|
|
787
|
+
* @returns {string} Body XML with OriginHost injected
|
|
788
|
+
*/
|
|
789
|
+
// eslint-disable-next-line class-methods-use-this
|
|
790
|
+
injectOriginHost(body, originHost) {
|
|
791
|
+
const meth = 'adapter-injectOriginHost';
|
|
792
|
+
const origin = `${this.id}-${meth}`;
|
|
793
|
+
log.trace(origin);
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
// Find the closing tag of the root element (ShPull, ShUpdate, etc.)
|
|
797
|
+
// OriginHost should be inserted just before this closing tag
|
|
798
|
+
const closingTagMatch = body.match(/<\/(sh:)?(ShPull|ShUpdate|ShSubs|ShNotif)>/);
|
|
799
|
+
|
|
800
|
+
if (closingTagMatch) {
|
|
801
|
+
const closingTag = closingTagMatch[0];
|
|
802
|
+
const insertPosition = body.lastIndexOf(closingTag);
|
|
803
|
+
|
|
804
|
+
// Insert OriginHost before the closing tag with proper indentation
|
|
805
|
+
const modifiedBody = `${body.substring(0, insertPosition)} ${originHost}\n${body.substring(insertPosition)}`;
|
|
806
|
+
|
|
807
|
+
log.debug(`${origin}: Injected OriginHost before ${closingTag}`);
|
|
808
|
+
return modifiedBody;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// If no recognized Metaswitch operation tag found, append at the end
|
|
812
|
+
// This handles cases where the body is a simple XML fragment
|
|
813
|
+
log.warn(`${origin}: No recognized Metaswitch operation tag found, appending OriginHost to body`);
|
|
814
|
+
return `${body}\n${originHost}`;
|
|
815
|
+
} catch (ex) {
|
|
816
|
+
log.error(`${origin}: Exception injecting OriginHost: ${ex.message}`);
|
|
817
|
+
// Return original body if injection fails (fail gracefully)
|
|
818
|
+
return `${body}\n${originHost}`;
|
|
819
|
+
}
|
|
752
820
|
}
|
|
753
821
|
|
|
754
822
|
/**
|
package/package.json
CHANGED
package/propertiesSchema.json
CHANGED
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
"systemx.customer.com"
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
|
+
"domain": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "domain name for OriginHost parameter (Metaswitch authentication)",
|
|
17
|
+
"default": "domain",
|
|
18
|
+
"examples": [
|
|
19
|
+
"customer.com",
|
|
20
|
+
"metaswitch.local"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"clientVersion": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "client version for OriginHost parameter (Metaswitch API version)",
|
|
26
|
+
"default": "1.0",
|
|
27
|
+
"examples": [
|
|
28
|
+
"1.0",
|
|
29
|
+
"1.6",
|
|
30
|
+
"2.0"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
14
33
|
"port": {
|
|
15
34
|
"type": "integer",
|
|
16
35
|
"description": "port on which to connect to the server",
|
package/report/adapterInfo.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0
|
|
3
|
-
"configLines":
|
|
2
|
+
"version": "1.2.0",
|
|
3
|
+
"configLines": 4722,
|
|
4
4
|
"scriptLines": 2523,
|
|
5
|
-
"codeLines":
|
|
6
|
-
"testLines":
|
|
7
|
-
"testCases":
|
|
8
|
-
"totalCodeLines":
|
|
5
|
+
"codeLines": 2750,
|
|
6
|
+
"testLines": 4416,
|
|
7
|
+
"testCases": 200,
|
|
8
|
+
"totalCodeLines": 9689,
|
|
9
9
|
"wfTasks": 29
|
|
10
10
|
}
|
|
@@ -1486,19 +1486,20 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1486
1486
|
}
|
|
1487
1487
|
}).timeout(attemptTimeout);
|
|
1488
1488
|
|
|
1489
|
-
it('should wrap XML payload with SOAP envelope and
|
|
1489
|
+
it('should wrap XML payload with SOAP envelope and inject OriginHost', (done) => {
|
|
1490
1490
|
try {
|
|
1491
|
-
const xmlPayload = '<
|
|
1491
|
+
const xmlPayload = '<sh:ShPull><UserIdentity>12345</UserIdentity></sh:ShPull>';
|
|
1492
1492
|
const result = a.wrapBodyInSoapEnvelope(xmlPayload, 'EAS');
|
|
1493
1493
|
|
|
1494
1494
|
assert.equal(result.error, undefined);
|
|
1495
1495
|
assert.notEqual(result.payload, undefined);
|
|
1496
1496
|
assert.equal(result.payload.includes('<soapenv:Envelope'), true);
|
|
1497
|
-
assert.equal(result.payload.includes('<soapenv:Header
|
|
1498
|
-
assert.equal(result.payload.includes('<wsse:Security'), true);
|
|
1499
|
-
assert.equal(result.payload.includes('<wsse:UsernameToken>'), true);
|
|
1497
|
+
assert.equal(result.payload.includes('<soapenv:Header/>'), true);
|
|
1500
1498
|
assert.equal(result.payload.includes('<soapenv:Body>'), true);
|
|
1501
|
-
assert.equal(result.payload.includes(
|
|
1499
|
+
assert.equal(result.payload.includes('<OriginHost>'), true);
|
|
1500
|
+
assert.equal(result.payload.includes('adminName='), true);
|
|
1501
|
+
assert.equal(result.payload.includes('password='), true);
|
|
1502
|
+
assert.equal(result.payload.includes('clientVersion='), true);
|
|
1502
1503
|
done();
|
|
1503
1504
|
} catch (error) {
|
|
1504
1505
|
log.error(`Test Failure: ${error}`);
|
|
@@ -1580,19 +1581,19 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1580
1581
|
|
|
1581
1582
|
it('should use correct namespaces for different API types', (done) => {
|
|
1582
1583
|
try {
|
|
1583
|
-
const xmlPayload = '<
|
|
1584
|
+
const xmlPayload = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
|
|
1584
1585
|
|
|
1585
1586
|
const easResult = a.wrapBodyInSoapEnvelope(xmlPayload, 'EAS');
|
|
1586
|
-
assert.equal(easResult.payload.includes('xmlns:sh="http://www.
|
|
1587
|
+
assert.equal(easResult.payload.includes('xmlns:sh="http://www.metaswitch.com/sdp/soap/sh"'), true);
|
|
1587
1588
|
|
|
1588
1589
|
const nseriesResult = a.wrapBodyInSoapEnvelope(xmlPayload, 'NSeries');
|
|
1589
|
-
assert.equal(nseriesResult.payload.includes('xmlns:sh="http://www.
|
|
1590
|
+
assert.equal(nseriesResult.payload.includes('xmlns:sh="http://www.metaswitch.com/nsrs/soap/sh"'), true);
|
|
1590
1591
|
|
|
1591
1592
|
const metaviewResult = a.wrapBodyInSoapEnvelope(xmlPayload, 'Metaview');
|
|
1592
|
-
assert.equal(metaviewResult.payload.includes('xmlns:sh="http://www.
|
|
1593
|
+
assert.equal(metaviewResult.payload.includes('xmlns:sh="http://www.metaswitch.com/ems/soap/sh"'), true);
|
|
1593
1594
|
|
|
1594
1595
|
const nwsapResult = a.wrapBodyInSoapEnvelope(xmlPayload, 'NWSAP');
|
|
1595
|
-
assert.equal(nwsapResult.payload.includes('xmlns:sh="http://www.
|
|
1596
|
+
assert.equal(nwsapResult.payload.includes('xmlns:sh="http://www.metaswitch.com/srb/soap/sh"'), true);
|
|
1596
1597
|
|
|
1597
1598
|
done();
|
|
1598
1599
|
} catch (error) {
|
|
@@ -1601,13 +1602,18 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1601
1602
|
}
|
|
1602
1603
|
}).timeout(attemptTimeout);
|
|
1603
1604
|
|
|
1604
|
-
it('should include WS-Security
|
|
1605
|
+
it('should NOT include WS-Security headers (Metaswitch uses OriginHost)', (done) => {
|
|
1605
1606
|
try {
|
|
1606
|
-
const xmlPayload = '<
|
|
1607
|
+
const xmlPayload = '<sh:ShPull><UserIdentity>test</UserIdentity></sh:ShPull>';
|
|
1607
1608
|
const result = a.wrapBodyInSoapEnvelope(xmlPayload, 'EAS');
|
|
1608
1609
|
|
|
1609
|
-
|
|
1610
|
-
assert.equal(result.payload.includes('
|
|
1610
|
+
// Should have empty header, not WS-Security
|
|
1611
|
+
assert.equal(result.payload.includes('<soapenv:Header/>'), true);
|
|
1612
|
+
assert.equal(result.payload.includes('<wsse:Security'), false);
|
|
1613
|
+
assert.equal(result.payload.includes('<wsse:UsernameToken>'), false);
|
|
1614
|
+
|
|
1615
|
+
// Should have OriginHost with credentials instead
|
|
1616
|
+
assert.equal(result.payload.includes('<OriginHost>'), true);
|
|
1611
1617
|
done();
|
|
1612
1618
|
} catch (error) {
|
|
1613
1619
|
log.error(`Test Failure: ${error}`);
|
|
@@ -1632,9 +1638,11 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1632
1638
|
const namespaces = a.getSoapNamespaces('EAS');
|
|
1633
1639
|
|
|
1634
1640
|
assert.equal(namespaces.includes('xmlns:soapenv='), true);
|
|
1635
|
-
assert.equal(namespaces.includes('xmlns:wsse='), true);
|
|
1636
|
-
assert.equal(namespaces.includes('xmlns:wsu='), true);
|
|
1637
1641
|
assert.equal(namespaces.includes('xmlns:sh='), true);
|
|
1642
|
+
assert.equal(namespaces.includes('http://www.metaswitch.com/sdp/soap/sh'), true);
|
|
1643
|
+
// Should NOT include WS-Security namespaces (Metaswitch uses OriginHost)
|
|
1644
|
+
assert.equal(namespaces.includes('xmlns:wsse='), false);
|
|
1645
|
+
assert.equal(namespaces.includes('xmlns:wsu='), false);
|
|
1638
1646
|
done();
|
|
1639
1647
|
} catch (error) {
|
|
1640
1648
|
log.error(`Test Failure: ${error}`);
|
|
@@ -1673,10 +1681,29 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1673
1681
|
}).timeout(attemptTimeout);
|
|
1674
1682
|
});
|
|
1675
1683
|
|
|
1676
|
-
describe('#
|
|
1677
|
-
it('should have a
|
|
1684
|
+
describe('#buildOriginHost', () => {
|
|
1685
|
+
it('should have a buildOriginHost function', (done) => {
|
|
1686
|
+
try {
|
|
1687
|
+
assert.equal(true, typeof a.buildOriginHost === 'function');
|
|
1688
|
+
done();
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
log.error(`Test Failure: ${error}`);
|
|
1691
|
+
done(error);
|
|
1692
|
+
}
|
|
1693
|
+
}).timeout(attemptTimeout);
|
|
1694
|
+
|
|
1695
|
+
it('should create OriginHost with credentials from adapter properties', (done) => {
|
|
1678
1696
|
try {
|
|
1679
|
-
|
|
1697
|
+
const result = a.buildOriginHost();
|
|
1698
|
+
|
|
1699
|
+
assert.equal(result.error, undefined);
|
|
1700
|
+
assert.notEqual(result.originHost, undefined);
|
|
1701
|
+
assert.equal(result.originHost.includes('<OriginHost>'), true);
|
|
1702
|
+
assert.equal(result.originHost.includes('</OriginHost>'), true);
|
|
1703
|
+
assert.equal(result.originHost.includes('adminName='), true);
|
|
1704
|
+
assert.equal(result.originHost.includes('password='), true);
|
|
1705
|
+
assert.equal(result.originHost.includes('clientVersion='), true);
|
|
1706
|
+
assert.equal(result.originHost.includes('ignoreSequenceNumber=true'), true);
|
|
1680
1707
|
done();
|
|
1681
1708
|
} catch (error) {
|
|
1682
1709
|
log.error(`Test Failure: ${error}`);
|
|
@@ -1684,18 +1711,43 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1684
1711
|
}
|
|
1685
1712
|
}).timeout(attemptTimeout);
|
|
1686
1713
|
|
|
1687
|
-
it('should
|
|
1714
|
+
it('should URL-encode credentials with special characters', (done) => {
|
|
1688
1715
|
try {
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
assert.equal(
|
|
1694
|
-
assert.
|
|
1695
|
-
|
|
1696
|
-
assert.equal(
|
|
1697
|
-
|
|
1698
|
-
|
|
1716
|
+
const result = a.buildOriginHost();
|
|
1717
|
+
|
|
1718
|
+
// If password contains &, it should be URL-encoded as %26
|
|
1719
|
+
// This test validates URL encoding is applied
|
|
1720
|
+
assert.equal(result.error, undefined);
|
|
1721
|
+
assert.notEqual(result.originHost, undefined);
|
|
1722
|
+
// The OriginHost value should be XML-escaped (& becomes &)
|
|
1723
|
+
assert.equal(result.originHost.includes('&adminName='), true);
|
|
1724
|
+
done();
|
|
1725
|
+
} catch (error) {
|
|
1726
|
+
log.error(`Test Failure: ${error}`);
|
|
1727
|
+
done(error);
|
|
1728
|
+
}
|
|
1729
|
+
}).timeout(attemptTimeout);
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
describe('#injectOriginHost', () => {
|
|
1733
|
+
it('should have an injectOriginHost function', (done) => {
|
|
1734
|
+
try {
|
|
1735
|
+
assert.equal(true, typeof a.injectOriginHost === 'function');
|
|
1736
|
+
done();
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
log.error(`Test Failure: ${error}`);
|
|
1739
|
+
done(error);
|
|
1740
|
+
}
|
|
1741
|
+
}).timeout(attemptTimeout);
|
|
1742
|
+
|
|
1743
|
+
it('should inject OriginHost before closing ShPull tag', (done) => {
|
|
1744
|
+
try {
|
|
1745
|
+
const body = '<sh:ShPull><UserIdentity>12345</UserIdentity></sh:ShPull>';
|
|
1746
|
+
const originHost = '<OriginHost>server@domain?adminName=test&password=test</OriginHost>';
|
|
1747
|
+
const result = a.injectOriginHost(body, originHost);
|
|
1748
|
+
|
|
1749
|
+
assert.equal(result.includes(originHost), true);
|
|
1750
|
+
assert.equal(result.indexOf(originHost) < result.indexOf('</sh:ShPull>'), true);
|
|
1699
1751
|
done();
|
|
1700
1752
|
} catch (error) {
|
|
1701
1753
|
log.error(`Test Failure: ${error}`);
|
|
@@ -1703,15 +1755,14 @@ describe('[unit] Metaswitch Adapter Test', () => {
|
|
|
1703
1755
|
}
|
|
1704
1756
|
}).timeout(attemptTimeout);
|
|
1705
1757
|
|
|
1706
|
-
it('should
|
|
1758
|
+
it('should inject OriginHost before closing ShUpdate tag', (done) => {
|
|
1707
1759
|
try {
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1760
|
+
const body = '<sh:ShUpdate><UserIdentity>12345</UserIdentity></sh:ShUpdate>';
|
|
1761
|
+
const originHost = '<OriginHost>server@domain?adminName=test&password=test</OriginHost>';
|
|
1762
|
+
const result = a.injectOriginHost(body, originHost);
|
|
1710
1763
|
|
|
1711
|
-
assert.equal(
|
|
1712
|
-
assert.equal(
|
|
1713
|
-
assert.equal(header.includes(`<${samProps.authentication.username}>`), false);
|
|
1714
|
-
assert.equal(header.includes(`&${samProps.authentication.password}"`), false);
|
|
1764
|
+
assert.equal(result.includes(originHost), true);
|
|
1765
|
+
assert.equal(result.indexOf(originHost) < result.indexOf('</sh:ShUpdate>'), true);
|
|
1715
1766
|
done();
|
|
1716
1767
|
} catch (error) {
|
|
1717
1768
|
log.error(`Test Failure: ${error}`);
|