@open-xchange/soap-client 0.0.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/.env.default ADDED
@@ -0,0 +1,16 @@
1
+ # URL to the App Suite Middleware for e2e-provisioning via SOAP
2
+ PROVISIONING_URL=https://appsuite-main.dev.oxui.de/
3
+
4
+ # URL to the App Suite UI for e2e-tests
5
+ LAUNCH_URL=https://core-ui-main.dev.oxui.de
6
+
7
+ # admin credentials for e2e middleware
8
+ E2E_ADMIN_USER=
9
+ E2E_ADMIN_PW=
10
+
11
+ # Flag to enable/disable debug output for SOAP requests
12
+ DEBUG_SOAP=false
13
+
14
+ MX_DOMAIN=box.ox.io
15
+
16
+ PROVSIONING_API=common
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import { program } from 'commander'
4
+ import dotenv from 'dotenv'
5
+ import * as secondaryAccountService from '../services/secondaryAccount.js'
6
+ import * as userService from '../services/common/user.js'
7
+ import * as contextService from '../services/common/context.js'
8
+
9
+ dotenv.config({ path: '.env' })
10
+ dotenv.config({ path: '.env.defaults' })
11
+
12
+ const contextAdmin = {
13
+ admin: {
14
+ name: 'oxadmin',
15
+ password: 'secret',
16
+ display_name: 'context admin',
17
+ sur_name: 'admin',
18
+ given_name: 'context',
19
+ email1: `oxadmin@${process.env.MX_DOMAIN}`,
20
+ primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`,
21
+ login: 'oxadmin'
22
+ }
23
+ }
24
+
25
+ function checkEnvironmentVariables () {
26
+ const neededEnvironmentVariables = ['PROVISIONING_URL', 'E2E_ADMIN_USER', 'E2E_ADMIN_PW', 'MX_DOMAIN']
27
+ for (const envVar of neededEnvironmentVariables) {
28
+ if (!process.env[envVar]) {
29
+ console.error(`Environment variable ${envVar} is required`)
30
+ process.exit(1)
31
+ }
32
+ }
33
+ }
34
+
35
+ async function getOrCreateContext (data) {
36
+ const contextData = { ...{ maxQuota: -1 }, ...data }
37
+ let context
38
+ try {
39
+ const existingContext = await contextService.list(contextData.id || contextData.name)
40
+
41
+ if (existingContext?.length > 0) {
42
+ console.log('Context already exists, using existing one')
43
+ context = existingContext[0]
44
+ } else {
45
+ console.log('Creating new context')
46
+ context = await contextService.create(contextData)
47
+ }
48
+ return {
49
+ ...context,
50
+ ...contextAdmin
51
+ }
52
+ } catch (error) {
53
+ console.error('Error in context operation:', error)
54
+ throw error
55
+ }
56
+ }
57
+
58
+ async function getOrCreateUser (context, userData) {
59
+ try {
60
+ const existingUser = await userService.list(context, userData.display_name)
61
+ if (existingUser?.length > 0) {
62
+ console.log('User already exists, using existing one')
63
+ return existingUser[0]
64
+ }
65
+ console.log('Creating new user')
66
+ return await userService.create(context, userData)
67
+ } catch (error) {
68
+ console.error('Error in user operation:', error)
69
+ throw error
70
+ }
71
+ }
72
+
73
+ async function getOrCreateSecondaryAccount (accountData, context, users) {
74
+ try {
75
+ const existingAccounts = (await secondaryAccountService.list(context, users)).filter(acc => acc.name === accountData.name)
76
+ if (existingAccounts?.length > 0) {
77
+ const usersWithoutAccount = users.filter(user => {
78
+ const hasAccount = existingAccounts.some(acc => acc.userId === user.id)
79
+ return !hasAccount
80
+ })
81
+ if (usersWithoutAccount.length === 0) return existingAccounts[0]
82
+ return await secondaryAccountService.create(accountData, context, usersWithoutAccount)
83
+ }
84
+ console.log('Creating new secondary account')
85
+ return await secondaryAccountService.create(accountData, context, users)
86
+ } catch (error) {
87
+ console.error('Error in secondary account operation:', error)
88
+ throw error
89
+ }
90
+ }
91
+
92
+ async function provisionFromFile (configPath) {
93
+ checkEnvironmentVariables()
94
+ let config
95
+ const createdUsers = []
96
+
97
+ try {
98
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
99
+ } catch (err) {
100
+ console.error('Failed to read config file:', err)
101
+ process.exit(1)
102
+ }
103
+
104
+ for (const contexts of config.contexts || []) {
105
+ try {
106
+ const contextData = contexts.data
107
+ const context = await getOrCreateContext(contexts.data.name)
108
+
109
+ if (contextData.capabilities) {
110
+ try {
111
+ await contextService.changeCapabilities(context.id, contextData.capabilities, undefined)
112
+ console.log('Changed capabilities for context:', context.id)
113
+ } catch (capError) {
114
+ console.error('Error changing capabilities for context', context.id, capError)
115
+ }
116
+ }
117
+
118
+ if (contextData.config) {
119
+ try {
120
+ await contextService.change({ id: context.id, userAttributes: contextData.config })
121
+ console.log('Changed config for context:', context.id)
122
+ } catch (cfgError) {
123
+ console.error('Error changing config for context:', context.id, cfgError)
124
+ }
125
+ }
126
+
127
+ for (const userData of (contexts.users || [])) {
128
+ try {
129
+ const user = await getOrCreateUser(context, userData)
130
+ createdUsers.push(user)
131
+ console.log('Provisioned user:', user && user.id)
132
+ } catch (userError) {
133
+ console.error('Error provisioning user', userData.display_name, userError)
134
+ }
135
+ }
136
+
137
+ for (const secondaryAccount of (contexts.secondaryAccounts || [])) {
138
+ try {
139
+ const userIds = createdUsers.map(user => ({ id: user.id }))
140
+ const secAccount = await getOrCreateSecondaryAccount(secondaryAccount, context, userIds)
141
+ console.log('Provisioned secondary account:', secAccount?.primaryAddress)
142
+ } catch (accError) {
143
+ console.error('Error provisioning secondary account for context', context.id, accError)
144
+ }
145
+ }
146
+ } catch (ctxError) {
147
+ console.error('Error provisioning context', contexts.name, ctxError)
148
+ }
149
+ }
150
+ }
151
+
152
+ program
153
+ .version('1.0.0')
154
+ .description('OX Provisioning Tool')
155
+
156
+ program
157
+ .option('-f, --file <path>', 'Path to provisioning JSON config file', './provisioning.json')
158
+ .action(async (options) => {
159
+ await provisionFromFile(options.file)
160
+ })
161
+
162
+ program
163
+ .command('create <resourceType>')
164
+ .description('Create a resource (context, user, account)')
165
+ .option('-n, --name <name>', 'Name (for context, user or account)')
166
+ .option('-c, --context-id <id>', 'Context ID (for user or account)')
167
+ .option('-e, --email <email>', 'Email address (for user and account)')
168
+ .option('-q, --quota <quota>', 'Max quota in MB (for context)')
169
+ .option('--capabilities <capabilities>', 'Comma-separated capabilities (for context)')
170
+ .option('-p, --password <password>', 'Password (for user)', 'secret')
171
+ .option('-g, --given-name <name>', 'Given name (for user)', 'Test')
172
+ .option('-s, --sur-name <name>', 'Surname (for user)', 'User')
173
+ .option('-d, --display-name <name>', 'Display name (for user)')
174
+ .option('-u, --users <ids>', 'Comma-separated user IDs (for account)')
175
+ .action(async (resourceType, options) => {
176
+ checkEnvironmentVariables()
177
+
178
+ try {
179
+ switch (resourceType) {
180
+ case 'context': {
181
+ const contextData = {
182
+ name: options.name,
183
+ maxQuota: options.quota ? parseInt(options.quota) : -1
184
+ }
185
+ const context = await getOrCreateContext(contextData)
186
+
187
+ if (options.capabilities) {
188
+ console.log('Setting capabilities for context:', options.capabilities)
189
+ await contextService.changeCapabilities(context.id, options.capabilities, undefined)
190
+ console.log('Capabilities set for context:', context.id)
191
+ }
192
+ break
193
+ }
194
+ case 'user': {
195
+ if (!options.contextId || !options.email) {
196
+ console.error('--context-id and --email is required for user creation')
197
+ process.exit(1)
198
+ }
199
+
200
+ const userData = {
201
+ name: options.name,
202
+ primaryEmail: options.email,
203
+ email1: options.email,
204
+ given_name: options.givenName || 'Test',
205
+ sur_name: options.surName || 'User',
206
+ display_name: options.displayName || 'Test User',
207
+ password: options.password || 'secret'
208
+ }
209
+
210
+ const context = await getOrCreateContext({ id: options.contextId })
211
+ await getOrCreateUser(context, userData)
212
+
213
+ break
214
+ }
215
+ case 'account': {
216
+ if (!options.contextId || !options.name || !options.email || !options.users) {
217
+ console.error('--name, --email, --context-id, and --users are required for account creation')
218
+ process.exit(1)
219
+ }
220
+
221
+ const accountData = {
222
+ name: options.name,
223
+ login: options.email,
224
+ primaryAddress: options.email,
225
+ mailEndpointSource: 'primary',
226
+ transportEndpointSource: 'primary',
227
+ personal: options.name
228
+ }
229
+
230
+ const contextData = await contextService.get(options.contextId)
231
+ const context = { ...contextData, ...contextAdmin }
232
+ const userIds = options.users.split(',').map(id => ({ id }))
233
+ await getOrCreateSecondaryAccount(accountData, context, userIds)
234
+ break
235
+ }
236
+ default:
237
+ console.error('Unknown resource type. Must be one of: context, user, account')
238
+ process.exit(1)
239
+ }
240
+ } catch (error) {
241
+ console.error(`Error creating ${resourceType}:`, error)
242
+ process.exit(1)
243
+ }
244
+ })
245
+
246
+ program
247
+ .command('delete <resourceType>')
248
+ .description('Delete a resource (context, user, account)')
249
+ .option('--id <id>', 'ID of the resource (for context or user)')
250
+ .option('-c, --context-id <id>', 'Context ID (for user or account)')
251
+ .option('-e, --email <email>', 'Primary address (for account)')
252
+ .option('-u, --users <ids>', 'Comma-separated user IDs (for account)')
253
+ .action(async (resourceType, options) => {
254
+ checkEnvironmentVariables()
255
+
256
+ try {
257
+ switch (resourceType) {
258
+ case 'context':
259
+ if (!options.id) {
260
+ console.error('--id is required for context deletion')
261
+ process.exit(1)
262
+ }
263
+ await contextService.remove(options.id)
264
+ console.log('Context deleted successfully:', options.id)
265
+ break
266
+
267
+ case 'user': {
268
+ if (!options.contextId || !options.id) {
269
+ console.error('--context-id and --id are required for user deletion')
270
+ process.exit(1)
271
+ }
272
+ const contextData = await contextService.get(options.contextId)
273
+ const context = { ...contextData, ...contextAdmin }
274
+ await userService.remove(context, options.id)
275
+ console.log('User deleted successfully:', options.id)
276
+ break
277
+ }
278
+ case 'account': {
279
+ if (!options.contextId || !options.email) {
280
+ console.error('--context-id and --primary-address are required for account deletion')
281
+ process.exit(1)
282
+ }
283
+ const contextData = await contextService.get(options.contextId)
284
+ const context = { ...contextData, ...contextAdmin }
285
+ const users = options.users ? options.users.split(',').map(id => ({ id })) : []
286
+ await secondaryAccountService.remove(options.email, context, users)
287
+ console.log('Secondary account deleted successfully:', options.primaryAddress)
288
+ break
289
+ }
290
+ default:
291
+ console.error('Unknown resource type. Must be one of: context, user, account')
292
+ process.exit(1)
293
+ }
294
+ } catch (error) {
295
+ console.error(`Error deleting ${resourceType}:`, error)
296
+ process.exit(1)
297
+ }
298
+ })
299
+
300
+ program.parse(process.argv)
@@ -0,0 +1,55 @@
1
+ {
2
+ "contexts": [
3
+ {
4
+ "data": {
5
+ "name": "test",
6
+ "capabilities": "switchboard,ai-service,spreadsheet,text,document_preview,presentation,presenter,remote_presenter",
7
+ "config": {
8
+ "entries": [{
9
+ "key": "config",
10
+ "value": {
11
+ "entries": [
12
+ { "key": "io.ox/core//ai/openai/useAzure", "value": "false" }
13
+ ]
14
+ }
15
+ }]
16
+ }
17
+ },
18
+ "users": [
19
+ {
20
+ "name": "testuser",
21
+ "primaryEmail": "testuser-123@box.ox.io",
22
+ "display_name": "Test User",
23
+ "imapLogin": "testuser-123",
24
+ "imapServer": "main-dovecot",
25
+ "smtpServer": "main-postfix",
26
+ "email1": "testuser-123@box.ox.io",
27
+ "password": "secret",
28
+ "sur_name": "User",
29
+ "given_name": "Test"
30
+ },
31
+ {
32
+ "name": "testuser124",
33
+ "primaryEmail": "testuser-124@box.ox.io",
34
+ "display_name": "Test User",
35
+ "imapLogin": "testuser-124",
36
+ "imapServer": "main-dovecot",
37
+ "smtpServer": "main-postfix",
38
+ "email1": "testuser-124@box.ox.io",
39
+ "password": "secret",
40
+ "sur_name": "User",
41
+ "given_name": "Test124"
42
+ }
43
+ ],
44
+ "secondaryAccount":
45
+ {
46
+ "name": "info123",
47
+ "login": "info123@box.ox.io",
48
+ "primaryAddress": "info123@box.ox.io",
49
+ "mailEndpointSource": "primary",
50
+ "transportEndpointSource": "primary",
51
+ "personal": "Info 123"
52
+ }
53
+ }
54
+ ]
55
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@open-xchange/soap-client",
3
+ "version": "0.0.1",
4
+ "description": "SOAP client for OX App Suite",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://gitlab.com/openxchange/appsuite/web-foundation/tools",
10
+ "directory": "packages/soap-client"
11
+ },
12
+ "exports": {
13
+ "./common": "./services/common/index.js",
14
+ "./reseller": "./services/reseller/index.js"
15
+ },
16
+ "bin": {
17
+ "provision": "./bin/provision.js"
18
+ },
19
+ "keywords": [],
20
+ "author": "",
21
+ "license": "AGPL-3.0-or-later",
22
+ "dependencies": {
23
+ "commander": "^13.1.0",
24
+ "dotenv": "^16.4.5",
25
+ "p-retry": "^6.2.1",
26
+ "soap": "^1.1.10"
27
+ },
28
+ "devDependencies": {
29
+ "@open-xchange/lint": "0.2.0"
30
+ },
31
+ "scripts": {
32
+ "lint": "eslint .",
33
+ "provision": "node ./bin/provision.js"
34
+ }
35
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync, logSoapError } from '../../soap.js'
22
+
23
+ const OXContextService = createClientAsync('OXContextService').then(client => client)
24
+
25
+ /**
26
+ * This function retrieves the default context from the OXContextService.
27
+ * @returns {Promise<Object>} The first result from the listAsync method call.
28
+ */
29
+ export async function getDefault () {
30
+ return (await (await OXContextService).listAsync({ search_pattern: 'defaultcontext' }))[0]
31
+ }
32
+
33
+ /**
34
+ * This function removes the context with the specified ID.
35
+ * @param {number} id The ID of the context to remove.
36
+ * @returns {Promise<Boolean>} Returns true if it could remove the context and false if it could not
37
+ */
38
+ export async function remove (id) {
39
+ return !!await (await OXContextService).deleteAsync({ ctx: { id } }).then(() => true).catch(logSoapError)
40
+ }
41
+
42
+ /**
43
+ * This function creates a new context.
44
+ * @param {Object} ctx The context to create.
45
+ * @param {Object} adminUser The admin user of the context.
46
+ * Defaults to:
47
+ * ```JSON
48
+ * {
49
+ name: 'oxadmin',
50
+ password: 'secret',
51
+ display_name: 'context admin',
52
+ sur_name: 'admin',
53
+ given_name: 'context',
54
+ email1: `oxadmin@${process.env.MX_DOMAIN}`,
55
+ primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
56
+ }
57
+ All properties are needed and will be inserted if not provided.
58
+ ```
59
+ * @returns {Promise<Object>} The created context.
60
+ */
61
+ export async function create (ctx = {}, adminUser = {}) {
62
+ return await (await OXContextService)
63
+ .createAsync({
64
+ ctx,
65
+ admin_user: Object.assign({
66
+ name: 'oxadmin',
67
+ password: 'secret',
68
+ display_name: 'context admin',
69
+ sur_name: 'admin',
70
+ given_name: 'context',
71
+ email1: `oxadmin@${process.env.MX_DOMAIN}`,
72
+ primaryEmail: `oxadmin@${process.env.MX_DOMAIN}`
73
+ }, adminUser)
74
+ })
75
+ .then(async context => {
76
+ await changeModuleAccessByName(context.id, 'all')
77
+ return context
78
+ })
79
+ .catch(e => {
80
+ logSoapError(e)
81
+ throw e
82
+ })
83
+ }
84
+
85
+ /**
86
+ * This function changes the module access of the specified context.
87
+ * @param {number} id The ID of the context.
88
+ * @param {string} access_combination_name The name of the access combination to set.
89
+ * @returns {Promise<void>}
90
+ */
91
+ // eslint-disable-next-line camelcase
92
+ export async function changeModuleAccessByName (id, access_combination_name) {
93
+ // eslint-disable-next-line camelcase
94
+ return await (await OXContextService).changeModuleAccessByNameAsync({ ctx: { id }, access_combination_name })
95
+ }
96
+
97
+ /**
98
+ * This function changes the capabilities of the specified context.
99
+ * @param {number} id The ID of the context.
100
+ * @param {string} capsToAdd The capabilities to add.
101
+ * @param {string} capsToRemove The capabilities to remove.
102
+ * @returns {Promise<void>}
103
+ */
104
+ export async function changeCapabilities (id, capsToAdd, capsToRemove) {
105
+ const data = {}
106
+ if (capsToAdd) data.capsToAdd = capsToAdd
107
+ if (capsToRemove) data.capsToRemove = capsToRemove
108
+ return await (await OXContextService).changeCapabilitiesAsync({ ctx: { id }, ...data })
109
+ }
110
+
111
+ /**
112
+ * This function retrieves the module access of the specified context.
113
+ * @param {number} id The ID of the context.
114
+ * @returns {Promise<Object>} The module access of the specified context.
115
+ */
116
+ export async function getModuleAccess (id) {
117
+ return await (await OXContextService).getModuleAccessAsync({ ctx: { id } })
118
+ }
119
+ /**
120
+ * This function changes the module access of the specified context.
121
+ * @param {number} id The ID of the context.
122
+ * @param {Object} moduleAccess The module access to set.
123
+ * @returns {Promise<void>}
124
+ */
125
+ export async function changeModuleAccess (id, moduleAccess) {
126
+ const currentAccess = await getModuleAccess(id)
127
+ return await (await OXContextService).changeModuleAccessAsync({ ctx: { id }, access: { ...currentAccess, ...moduleAccess } })
128
+ }
129
+
130
+ /**
131
+ * Search for contexts.
132
+ * @param {String} searchPattern The pattern to search for
133
+ * @param {Object} options Additional options
134
+ * @param {Boolean} options.excludeDisabled Exclude disabled contexts from the search results (default: true)
135
+ * @returns {Promise<Array<Object>>} The list of contexts that match the search pattern.
136
+ */
137
+ export async function list (searchPattern, { excludeDisabled } = { excludeDisabled: true }) {
138
+ return await (await OXContextService).listAsync({ search_pattern: searchPattern, exclude_disabled: excludeDisabled })
139
+ }
140
+
141
+ /**
142
+ * This function retrieves the context with the specified ID.
143
+ * @param {number} id The ID of the context to retrieve.
144
+ * @returns {Promise<Object>} The context with the specified ID.
145
+ */
146
+ export async function get (id) {
147
+ return await (await OXContextService).getDataAsync({ ctx: { id } })
148
+ }
149
+
150
+ /**
151
+ * This function changes the context with the specified ID.
152
+ * @param {Object} ctx The context to change.
153
+ * @returns {Promise<void>}
154
+ */
155
+ export async function change (ctx) {
156
+ return await (await OXContextService).changeAsync({ ctx })
157
+ }
@@ -0,0 +1,11 @@
1
+ import * as contextService from './context.js'
2
+ import * as userService from './user.js'
3
+ import { getFilestorageId } from './util.js'
4
+ import * as secondaryAccountService from '../secondaryAccount.js'
5
+
6
+ export {
7
+ contextService,
8
+ userService,
9
+ secondaryAccountService,
10
+ getFilestorageId
11
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync, logSoapError } from '../../soap.js'
22
+
23
+ const OXUserService = createClientAsync('OXUserService').then(client => client)
24
+
25
+ /**
26
+ * This function removes the user with the specified context and user ID.
27
+ * @param {Object} context The context of the user to be removed.
28
+ * @param {number} userId The ID of the user to be removed.
29
+ * @returns {Promise<Boolean>} Returns true if it could remove the user and false if it could not
30
+ */
31
+ export async function remove (context, userId) {
32
+ return !!await (await OXUserService).deleteAsync({
33
+ ctx: { id: context.id },
34
+ user: { id: userId },
35
+ auth: context.admin
36
+ }).then(() => true).catch(logSoapError)
37
+ }
38
+
39
+ /**
40
+ * This function creates a new user.
41
+ * @param {Object} context The context of the user to be created.
42
+ * @param {Object} usrdata The user to create.
43
+ * @returns {Promise<Object>} The created user.
44
+ */
45
+ export async function create (context, usrdata) {
46
+ return await (await OXUserService).createAsync({
47
+ ctx: { id: context.id },
48
+ usrdata,
49
+ auth: context.admin
50
+ })
51
+ }
52
+
53
+ /**
54
+ * This function changes the context with the specified ID.
55
+ * @param {Object} context The context of the user to be changed.
56
+ * @param {Object} usrdata The user to change.
57
+ * @returns {Promise<void>}
58
+ */
59
+ export async function change (context, usrdata) {
60
+ return await (await OXUserService).changeAsync({
61
+ ctx: { id: context.id },
62
+ usrdata,
63
+ auth: context.admin
64
+ })
65
+ }
66
+
67
+ export async function changeByModuleAccess (context, userId, currentAccess, moduleAccess) {
68
+ return await (await OXUserService).changeByModuleAccessAsync({
69
+ ctx: { id: context.id },
70
+ moduleAccess: Object.assign({}, currentAccess, moduleAccess),
71
+ user: { id: userId },
72
+ auth: context.admin
73
+ })
74
+ }
75
+
76
+ export async function changeByModuleAccessName (context, userId, accessCombinationName) {
77
+ return await (await OXUserService).changeByModuleAccessNameAsync({
78
+ ctx: { id: context.id },
79
+ access_combination_name: accessCombinationName,
80
+ user: { id: userId },
81
+ auth: context.admin
82
+ }).catch(error => console.error('Module access change error:', error))
83
+ }
84
+
85
+ export async function getModuleAccess (context, userId) {
86
+ return await (await OXUserService).getModuleAccessAsync({
87
+ ctx: { id: context.id },
88
+ user: { id: userId },
89
+ auth: context.admin
90
+ }).then(
91
+ (res) => res
92
+ )
93
+ }
94
+
95
+ export async function changeCapabilities (context, userId, capsToAdd, capsToRemove) {
96
+ const data = {
97
+ ctx: { id: context.id },
98
+ user: { id: userId },
99
+ auth: context.admin
100
+ }
101
+ if (capsToAdd) data.capsToAdd = capsToAdd
102
+ if (capsToRemove) data.capsToRemove = capsToRemove
103
+ return await (await OXUserService).changeCapabilitiesAsync(data)
104
+ }
105
+
106
+ export async function exists (context, userId) {
107
+ return await (await OXUserService).existsAsync({
108
+ ctx: { id: context.id },
109
+ user: { id: userId },
110
+ auth: context.admin
111
+ })
112
+ }
113
+
114
+ export async function list (context, searchPattern, includeGuests = false) {
115
+ return await (await OXUserService).listAsync({
116
+ ctx: { id: context.id },
117
+ search_pattern: searchPattern,
118
+ include_guests: includeGuests,
119
+ auth: context.admin
120
+ })
121
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync } from '../../soap.js'
22
+
23
+ /**
24
+ * This function retrieves the ID of the first filestorage.
25
+ * @returns {Promise<number>} The ID of the first filestorage.
26
+ */
27
+ let fileStoreId
28
+
29
+ export async function getFilestorageId () {
30
+ const OXUtilService = createClientAsync('OXUtilService').then(client => client)
31
+
32
+ if (fileStoreId) return fileStoreId
33
+ fileStoreId = (await (await OXUtilService).listFilestoreAsync())[0]?.id
34
+ return fileStoreId
35
+ }
@@ -0,0 +1,11 @@
1
+ import * as oxaasService from './oxaas.js'
2
+ import * as resellerContextService from './resellerContext.js'
3
+ import * as resellerUserService from './resellerUser.js'
4
+ import * as secondaryAccountService from '../secondaryAccount.js'
5
+
6
+ export {
7
+ oxaasService,
8
+ resellerContextService,
9
+ resellerUserService,
10
+ secondaryAccountService
11
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync } from '../../soap.js'
22
+
23
+ const OXaaSService = createClientAsync('OXaaSService').then(client => client)
24
+
25
+ export async function setMailQuota (data) {
26
+ return await (await OXaaSService).setMailQuotaAsync(data)
27
+ }
28
+
29
+ export async function createSharedDomain (data) {
30
+ return await (await OXaaSService).createSharedDomain(data)
31
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync, logSoapError } from '../../soap.js'
22
+
23
+ const OXResellerContextService = createClientAsync('OXResellerContextService').then(client => client)
24
+
25
+ export async function create (data) {
26
+ return await (await OXResellerContextService).createAsync(data)
27
+ }
28
+
29
+ export async function remove (contextId) {
30
+ return !!await (await OXResellerContextService).deleteAsync({
31
+ ctx: { id: contextId }
32
+ }).then(() => true).catch(logSoapError)
33
+ }
34
+
35
+ export async function change (ctx) {
36
+ return await (await OXResellerContextService).changeAsync({ ctx })
37
+ }
38
+
39
+ export async function get (id) {
40
+ return await (await OXResellerContextService).getDataAsync({ ctx: { id } })
41
+ }
42
+
43
+ export async function getModuleAccess (id) {
44
+ return await (await OXResellerContextService).getModuleAccessAsync({ ctx: id })
45
+ }
46
+
47
+ export async function changeModuleAccess (id, access) {
48
+ const currentAccess = await getModuleAccess(id)
49
+
50
+ return await (await OXResellerContextService).changeModuleAccessAsync({ ctx: { id }, access: { ...currentAccess, ...access } })
51
+ }
52
+
53
+ export async function listAll (data) {
54
+ return await (await OXResellerContextService).listAllAsync(data)
55
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { createClientAsync, logSoapError } from '../../soap.js'
22
+
23
+ const OXResellerUserService = createClientAsync('OXResellerUserService').then(client => client)
24
+
25
+ export async function remove (contextId, userId) {
26
+ return !!await (await OXResellerUserService).deleteAsync({
27
+ ctx: { id: contextId },
28
+ user: { id: userId }
29
+ }).then(() => true).catch(logSoapError)
30
+ }
31
+
32
+ /**
33
+ * This function changes the context with the specified ID.
34
+ * @param {Object} context The context of the user to be changed.
35
+ * @param {Object} usrdata The user to change.
36
+ * @returns {Promise<void>}
37
+ */
38
+ export async function change (context, usrdata) {
39
+ return await (await OXResellerUserService).changeAsync({
40
+ ctx: { id: context.id },
41
+ usrdata,
42
+ auth: { login: context.admin.name, password: context.admin.password }
43
+ })
44
+ }
45
+
46
+ export async function get (data) {
47
+ return await (await OXResellerUserService).getDataAsync(data)
48
+ }
49
+
50
+ export async function getModuleAccess (context, userId) {
51
+ return await (await OXResellerUserService).getModuleAccessAsync({
52
+ ctx: { id: context.id },
53
+ user: { id: userId },
54
+ auth: { login: context.admin.name, password: context.admin.password }
55
+ }).then(
56
+ (res) => res
57
+ )
58
+ }
59
+
60
+ export async function changeByModuleAccess (context, userId, currentAccess, moduleAccess) {
61
+ return await (await OXResellerUserService).changeByModuleAccessAsync({
62
+ ctx: { id: context.id },
63
+ moduleAccess: Object.assign({}, currentAccess, moduleAccess),
64
+ user: { id: userId },
65
+ auth: { login: context.admin.name, password: context.admin.password }
66
+ })
67
+ }
68
+
69
+ export async function changeByModuleAccessName (context, userId, accessCombinationName) {
70
+ return await (await OXResellerUserService).changeByModuleAccessNameAsync({
71
+ ctx: { id: context.id },
72
+ access_combination_name: accessCombinationName,
73
+ user: { id: userId },
74
+ auth: { login: context.admin.name, password: context.admin.password }
75
+ }).catch(error => console.error('Module access change error: ', error))
76
+ }
77
+
78
+ export async function createByModuleAccessName (context, usrdata) {
79
+ return await (await OXResellerUserService).createByModuleAccessNameAsync({
80
+ ctx: { id: context.id },
81
+ usrdata,
82
+ access_combination_name: 'all',
83
+ auth: { login: context.admin.name, password: context.admin.password }
84
+ })
85
+ }
86
+
87
+ export async function listAll (data) {
88
+ return await (await OXResellerUserService).listAllAsync(data)
89
+ }
@@ -0,0 +1,48 @@
1
+ import { createClientAsync } from '../soap.js'
2
+
3
+ const OXSecondaryAccountService = createClientAsync('OXSecondaryAccountService').then(client => client)
4
+
5
+ /**
6
+ * Creates a secondary account using the OX Secondary Account Service
7
+ * @async
8
+ * @param {Object} accountData - The data for the secondary account to be created
9
+ * @param {Object} context - The context object where the account will be created
10
+ * @param {Array<Object>} users - Array of users to associate with the secondary account
11
+ * @param {Array<Object>} groups - Array of groups to associate with the secondary account
12
+ * @returns {Promise<Object>} The created secondary account object
13
+ * @throws {Error} If the account creation fails
14
+ */
15
+ export async function create (accountData, context, users = [], groups = []) {
16
+ return await (await OXSecondaryAccountService).createAsync({
17
+ accountDataOnCreate: accountData,
18
+ context: { id: context.id },
19
+ users: users || [],
20
+ auth: context.admin
21
+ })
22
+ }
23
+
24
+ export async function list (context) {
25
+ return await (await OXSecondaryAccountService).listAsync({
26
+ context: { id: context.id },
27
+ auth: context.admin
28
+ })
29
+ }
30
+ export async function remove (primaryAddress, context, users, groups) {
31
+ return await (await OXSecondaryAccountService).deleteAsync({
32
+ primaryAddress,
33
+ context: { id: context.id },
34
+ users,
35
+ auth: context.admin
36
+ })
37
+ }
38
+
39
+ export async function update (primaryAddress, accountData, context, users, groups) {
40
+ return await (await OXSecondaryAccountService).updateAsync({
41
+ primaryAddress,
42
+ accountDataUpdate: accountData,
43
+ context: { id: context.id },
44
+ users,
45
+ groups,
46
+ auth: context.admin
47
+ })
48
+ }
package/soap.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
3
+ * @license AGPL-3.0
4
+ *
5
+ * This code is free software: you can redistribute it and/or modify
6
+ * it under the terms of the GNU Affero General Public License as published by
7
+ * the Free Software Foundation, either version 3 of the License, or
8
+ * (at your option) any later version.
9
+ *
10
+ * This program is distributed in the hope that it will be useful,
11
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ * GNU Affero General Public License for more details.
14
+ *
15
+ * You should have received a copy of the GNU Affero General Public License
16
+ * along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
17
+ *
18
+ * Any use of the work other than as authorized under this license or copyright law is prohibited.
19
+ */
20
+
21
+ import { performance, PerformanceObserver } from 'node:perf_hooks'
22
+ import * as SOAP from 'soap'
23
+ import dotenv from 'dotenv'
24
+ import pRetry, { AbortError as RetryAbortError } from 'p-retry'
25
+
26
+ // Set AbortError correctly
27
+ const AbortError = RetryAbortError
28
+
29
+ dotenv.config({ path: '.env' })
30
+ dotenv.config({ path: '.env.defaults' })
31
+
32
+ // This flag enables debug output including timing information.
33
+ const debug = process.env.DEBUG_SOAP === 'true'
34
+
35
+ // This URL is used to create the SOAP client.
36
+ const provisioningUrl = String(process.env.PROVISIONING_URL).replace(/\/$/, '')
37
+
38
+ // This user is used to authenticate against the provisioning API.
39
+ const provisioningAuth = {
40
+ auth: {
41
+ login: process.env.E2E_ADMIN_USER,
42
+ password: process.env.E2E_ADMIN_PW
43
+ }
44
+ }
45
+
46
+ /**
47
+ * This code creates a PerformanceObserver instance which asynchronously observes
48
+ * performance measurement events. It logs the name of the event (which is the name
49
+ * of the SOAP method in this context) and the time it took to execute in milliseconds.
50
+ *
51
+ * This is used for performance tracking of the SOAP methods.
52
+ * @see https://nodejs.org/api/perf_hooks.html
53
+ * @params {PerformanceObserverEntryList} items The list of performance entries.
54
+ * @returns {void}
55
+ */
56
+ const obs = new PerformanceObserver((items) => {
57
+ items.getEntries().forEach((entry) => {
58
+ if (debug) console.log(`${entry.name} took ${Math.round(entry.duration)}ms`)
59
+ })
60
+ })
61
+ obs.observe({ entryTypes: ['measure'] })
62
+
63
+ async function logSoapError (e) {
64
+ console.error(e?.originalError?.root?.Envelope?.Body?.Fault?.faultstring || e.message)
65
+ }
66
+
67
+ /**
68
+ * This function checks if the specified error should abort the retry.
69
+ * @param {Error} error The error to check.
70
+ * @returns {boolean} True if the error should abort the retry, false otherwise.
71
+ **/
72
+ function shouldAbortRetry (error) {
73
+ try {
74
+ const fault = error.root.Envelope.Body.Fault
75
+ const details = fault.detail
76
+ const blockedFaultStrings = [
77
+ /Context \d+ already exists/,
78
+ /Authentication failed/,
79
+ /Context \d+ does not exist/,
80
+ /CloudPluginException: username .* already exists/,
81
+ /CloudPluginException: context name must begin/,
82
+ /The displayname is already used/,
83
+ /already exists in this context/,
84
+ /Shared Domain already exists/,
85
+ /Shared Domain already in use/,
86
+ /No such user/,
87
+ /Mandatory fields in context not set/,
88
+ /A mapping with login info .* already exists/,
89
+ /Id must not be set if pre-assembled context should be used/,
90
+ /Unmarshalling Error/
91
+ ]
92
+ const blockedExceptions = [
93
+ 'ContextExistsException',
94
+ 'InvalidCredentialsException',
95
+ 'NoSuchContextException'
96
+ ]
97
+ // return "details object contains any of the blocked items"
98
+ return blockedFaultStrings.some(msg => msg.test(fault.faultstring)) ||
99
+ blockedExceptions.some(exception => Object.hasOwnProperty.call(details, exception))
100
+ } catch (e) {
101
+ // some other error which is never blocked
102
+ return false
103
+ }
104
+ }
105
+
106
+ /**
107
+ * This function creates a SOAP client for the specified service type.
108
+ * @param {string} type The name of the service type.
109
+ * @returns {Promise<Object>} The SOAP client.
110
+ */
111
+ async function createClientAsync (type) {
112
+ const startMark = `${type}-start`
113
+ const endMark = `${type}-end`
114
+ performance.mark(startMark)
115
+ const endpoint = `${provisioningUrl}/webservices/${type}`
116
+ const url = `${endpoint}/?wsdl`
117
+ const client = await SOAP.createClientAsync(url, {
118
+ endpoint,
119
+ suppressStack: true,
120
+ wsdl_options: {
121
+ forever: true
122
+ },
123
+ gzip: true
124
+ })
125
+
126
+ // https://stackoverflow.com/questions/30740415/namespace-for-array-field-in-node-soap-client-node-js
127
+ client.wsdl.definitions.xmlns.ns1 = 'http://dataobjects.soap.admin.openexchange.com/xsd'
128
+ client.wsdl.xmlnsInEnvelope = client.wsdl._xmlnsMap()
129
+ performance.mark(endMark)
130
+ performance.measure(` ⏱ SOAP: ${type} -> createClientAsync`, startMark, endMark)
131
+
132
+ // This proxy effectively adds error handling, authentication and performance measurements to all methods of the SOAP client.
133
+ return new Proxy(client, {
134
+ get (target, prop, receiver) {
135
+ const origMethod = Reflect.get(target, prop, receiver)
136
+ if (typeof origMethod === 'function') {
137
+ return async function (options, clientOptions, ...args) {
138
+ const startMark = `${prop}-start`
139
+ const endMark = `${prop}-end`
140
+ performance.mark(startMark)
141
+ const soapOptions = { ...provisioningAuth, ...options }
142
+ if (soapOptions.auth) {
143
+ // only send login and password instead of complete admin object.
144
+ // this can fail because of ambiguous namespacing
145
+ soapOptions.auth = { login: soapOptions.auth.login, password: soapOptions.auth.password }
146
+ }
147
+
148
+ const result = await pRetry(() => origMethod.apply(this, [soapOptions, { timeout: 10000, ...clientOptions }, ...args]), {
149
+ retries: 3,
150
+ onFailedAttempt: async error => {
151
+ if (shouldAbortRetry(error)) throw new AbortError(error)
152
+ console.log(`Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.`)
153
+ }
154
+ }).catch(e => {
155
+ const soapError = e?.originalError?.root?.Envelope?.Body?.Fault
156
+ throw new Error(soapError?.faultstring || e.message)
157
+ })
158
+ performance.mark(endMark)
159
+ performance.measure(` ⏱ SOAP: ${type} -> ${String(prop)}`, startMark, endMark)
160
+ // Return only the first result from the SOAP method call, as we don't need the SOAP envelope and other stuff.
161
+ if (!result || !result[0]) return
162
+ return result[0]?.return
163
+ }
164
+ } else {
165
+ return origMethod
166
+ }
167
+ }
168
+ })
169
+ }
170
+
171
+ export { createClientAsync, logSoapError }