soajs.controller 4.3.17 → 4.3.19

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.
Files changed (35) hide show
  1. package/.github/workflows/ci.yml +4 -1
  2. package/.github/workflows/release.yml +4 -1
  3. package/ARCHITECTURE.md +1 -0
  4. package/docs/configuration-custom-registry.md +206 -6
  5. package/docs/middleware/idempotency.md +31 -2
  6. package/docs/middleware/mt.md +18 -0
  7. package/docs/multi-tenant-security.md +1 -0
  8. package/docs/tenant-access-resolver.md +81 -0
  9. package/mw/gotoService/redirectToService.js +20 -8
  10. package/mw/idempotency/index.js +42 -4
  11. package/mw/idempotency/model/memory.js +12 -2
  12. package/mw/idempotency/model/mongo.js +10 -3
  13. package/mw/mt/tenantAccess.js +173 -0
  14. package/mw/mt/urac.js +61 -12
  15. package/package.json +1 -1
  16. package/test/data/provision/customRegistry/gateway.js +32 -0
  17. package/test/data/provision/customRegistry/urac.js +22 -0
  18. package/test/data/provision/environment/dev.js +9 -0
  19. package/test/data/provision/hosts/departments.js +12 -0
  20. package/test/data/provision/hosts/idempotent.js +12 -0
  21. package/test/data/provision/marketplace/departments.js +38 -0
  22. package/test/data/provision/marketplace/idempotent.js +37 -0
  23. package/test/data/provision/oauth/token/department_member_token.js +72 -0
  24. package/test/data/provision/oauth/token/department_outsider_token.js +72 -0
  25. package/test/data/provision/products/dsbrd.js +25 -0
  26. package/test/data/provision/tenants/dbtn.js +6 -0
  27. package/test/integration/UC5-tenant-access/index.js +153 -0
  28. package/test/integration/UC6-idempotency/index.js +197 -0
  29. package/test/integration/index.js +8 -0
  30. package/test/unit/index.js +2 -0
  31. package/test/unit/mw/idempotency/index.js +122 -0
  32. package/test/unit/mw/idempotency/memory.js +36 -4
  33. package/test/unit/mw/idempotency/mongo.js +62 -0
  34. package/test/unit/mw/mt/tenantAccess.js +338 -0
  35. package/test/unit/mw/mt/urac.js +229 -1
@@ -58,7 +58,7 @@ let model = {
58
58
  }
59
59
  },
60
60
 
61
- 'lock': async (key, ttl) => {
61
+ 'lock': async (key, ttl, lockId) => {
62
62
  let condition = {
63
63
  'l1': key.l1,
64
64
  'l2': key.l2
@@ -73,6 +73,7 @@ let model = {
73
73
  'l1': key.l1,
74
74
  'l2': key.l2,
75
75
  'status': 'in_flight',
76
+ 'lockId': lockId || null,
76
77
  'lockedAt': new Date(),
77
78
  'expiresAt': new Date(Date.now() + ttl)
78
79
  };
@@ -108,11 +109,17 @@ let model = {
108
109
  }
109
110
  },
110
111
 
111
- 'unlock': async (key) => {
112
+ //NOTE: only a record still in flight is removed, never a completed one, and with a lockId only
113
+ // the lock of that request, not one taken by a retry after the ttl
114
+ 'unlock': async (key, lockId) => {
112
115
  let condition = {
113
116
  'l1': key.l1,
114
- 'l2': key.l2
117
+ 'l2': key.l2,
118
+ 'status': 'in_flight'
115
119
  };
120
+ if (lockId) {
121
+ condition.lockId = lockId;
122
+ }
116
123
  try {
117
124
  await model.mongo.deleteOne(idempotency_store, condition);
118
125
  } catch (e) {
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @license
5
+ * Copyright SOAJS All Rights Reserved.
6
+ *
7
+ * Use of this source code is governed by an Apache license that can be
8
+ * found in the LICENSE file at the root of this repository
9
+ */
10
+
11
+ const { httpRequestLight } = require("../../lib/request.js");
12
+
13
+ const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
14
+
15
+ const DEFAULT_TIMEOUT = 2000;
16
+
17
+ /**
18
+ * Find the resolver that decides who can access the tenant of this request.
19
+ *
20
+ * NOTE: the tenant profile names the resolver, the urac custom registry defines what to call,
21
+ * so the call can move without touching every tenant
22
+ *
23
+ * @param soajs
24
+ * @returns {null|{name, subject, service, version, method, api, timeout}}
25
+ */
26
+ function getResolver(soajs) {
27
+ let access = get(["tenant", "profile", "access"], soajs);
28
+ if (!access || !access.resolver) {
29
+ return null;
30
+ }
31
+ let resolver = get(["registry", "custom", "urac", "value", "tenantAccessResolvers", access.resolver], soajs);
32
+ if (!resolver || !resolver.service || !resolver.api) {
33
+ if (soajs.log && soajs.log.error) {
34
+ soajs.log.error("Tenant access resolver [" + access.resolver + "] of tenant [" + soajs.tenant.code + "] is not defined in the urac custom registry");
35
+ }
36
+ return null;
37
+ }
38
+ return {
39
+ "name": access.resolver,
40
+ "subject": access.subject || null,
41
+ "service": resolver.service,
42
+ "version": resolver.version || null,
43
+ "method": resolver.method || "post",
44
+ "api": resolver.api,
45
+ "timeout": resolver.timeout || DEFAULT_TIMEOUT
46
+ };
47
+ }
48
+
49
+ /**
50
+ * The service behind the resolver has not been through the gateway, give it the tenant and key
51
+ * of this request the same way the gateway does, without any ACL or user
52
+ *
53
+ * @param soajs
54
+ * @param serviceName
55
+ * @returns {string}
56
+ */
57
+ function buildInjectObj(soajs, serviceName) {
58
+ let tenant = soajs.tenant;
59
+ let injectObj = {
60
+ "tenant": {
61
+ "id": tenant.id,
62
+ "name": tenant.name,
63
+ "code": tenant.code,
64
+ "locked": tenant.locked,
65
+ "type": tenant.type
66
+ },
67
+ "key": {
68
+ "config": {},
69
+ "iKey": tenant.key.iKey,
70
+ "eKey": tenant.key.eKey
71
+ },
72
+ "application": {
73
+ "product": tenant.application.product,
74
+ "package": tenant.application.package,
75
+ "appId": tenant.application.appId
76
+ }
77
+ };
78
+ if (tenant.main) {
79
+ injectObj.tenant.main = tenant.main;
80
+ }
81
+ if (soajs.servicesConfig) {
82
+ if (soajs.servicesConfig.commonFields) {
83
+ injectObj.key.config.commonFields = soajs.servicesConfig.commonFields;
84
+ }
85
+ if (soajs.servicesConfig[serviceName]) {
86
+ injectObj.key.config[serviceName] = soajs.servicesConfig[serviceName];
87
+ }
88
+ }
89
+ return JSON.stringify(injectObj);
90
+ }
91
+
92
+ /**
93
+ * Ask the resolver if the user has access to the tenant of this request and with what groups.
94
+ *
95
+ * NOTE: anything but a well formed yes is no access, the caller falls back to the urac driver
96
+ * so an invitation made after the token was issued still works
97
+ *
98
+ * @param soajs
99
+ * @param user the user from the token
100
+ * @param resolver from getResolver
101
+ * @param cb (result) result is {groups, groupsConfig} or null
102
+ */
103
+ function resolve(soajs, user, resolver, cb) {
104
+ let done = (result) => {
105
+ //NOTE: out of the promise, an error thrown further down must not come back here as a rejection
106
+ process.nextTick(() => {
107
+ return cb(result);
108
+ });
109
+ };
110
+ let label = "Tenant access resolver [" + resolver.name + "] for user [" + user.username + "] and tenant [" + soajs.tenant.code + "]";
111
+
112
+ if (!soajs.registry || !soajs.registry.services ||
113
+ !soajs.registry.services[resolver.service] || !soajs.registry.services[resolver.service].port) {
114
+ soajs.log.error(label + " failed, service [" + resolver.service + "] is not in the registry");
115
+ return done(null);
116
+ }
117
+ soajs.awareness.getHost(resolver.service, resolver.version, (host) => {
118
+ if (!host) {
119
+ soajs.log.error(label + " failed, no host for [" + resolver.service + "@" + resolver.version + "]");
120
+ return done(null);
121
+ }
122
+ let data = {
123
+ "user": {
124
+ "id": user.id || (user._id ? user._id.toString() : null),
125
+ "username": user.username,
126
+ "tenant": {
127
+ "id": user.tenant.id,
128
+ "code": user.tenant.code
129
+ }
130
+ },
131
+ "tenant": {
132
+ "id": soajs.tenant.id,
133
+ "code": soajs.tenant.code
134
+ },
135
+ "subject": resolver.subject
136
+ };
137
+ if (soajs.tenant.main) {
138
+ data.tenant.main = soajs.tenant.main;
139
+ }
140
+ httpRequestLight({
141
+ "uri": "http://" + host + ":" + soajs.registry.services[resolver.service].port + resolver.api,
142
+ "method": resolver.method,
143
+ "data": data,
144
+ "headers": { "soajsinjectobj": buildInjectObj(soajs, resolver.service) },
145
+ "timeout": resolver.timeout
146
+ }).then((response) => {
147
+ let answer = (response && response.result === true) ? response.data : null;
148
+ if (!answer || typeof answer !== "object") {
149
+ soajs.log.error(label + " returned an invalid response");
150
+ return done(null);
151
+ }
152
+ if (answer.access !== true) {
153
+ return done(null);
154
+ }
155
+ if (!answer.groupsConfig || !answer.groupsConfig.allowedPackages || typeof answer.groupsConfig.allowedPackages !== "object") {
156
+ soajs.log.error(label + " granted access without groupsConfig.allowedPackages");
157
+ return done(null);
158
+ }
159
+ return done({
160
+ "groups": Array.isArray(answer.groups) ? answer.groups : [],
161
+ "groupsConfig": answer.groupsConfig
162
+ });
163
+ }).catch((error) => {
164
+ soajs.log.error(label + " failed: " + error.message);
165
+ return done(null);
166
+ });
167
+ });
168
+ }
169
+
170
+ module.exports = {
171
+ "getResolver": getResolver,
172
+ "resolve": resolve
173
+ };
package/mw/mt/urac.js CHANGED
@@ -13,6 +13,7 @@ const coreModules = require("soajs.core.modules");
13
13
  let provision = coreModules.provision;
14
14
 
15
15
  const lib = require("./lib.js");
16
+ const tenantAccess = require("./tenantAccess.js");
16
17
 
17
18
  const get = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);
18
19
 
@@ -57,6 +58,31 @@ function Urac(param) {
57
58
  _self.username = param.username;
58
59
  }
59
60
  }
61
+
62
+ //NOTE: a user switching to a tenant that names a resolver in its profile is decided by the resolver,
63
+ // unless the token already shows an invitation to that tenant, invitations keep going through
64
+ // the urac driver
65
+ _self.tenantAccess = null;
66
+ if (!_self.userRecord && _self.id && _self.bearerTokenUser && _self.soajs.tenant && !_self.soajs.tenant.roaming) {
67
+ let user = _self.bearerTokenUser;
68
+ if (user.tenant && user.tenant.id !== _self.soajs.tenant.id && !isInvited(user, _self.soajs.tenant.id)) {
69
+ _self.tenantAccess = tenantAccess.getResolver(_self.soajs);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Check the allowedTenants of the user from the token
76
+ * @param user
77
+ * @param tenantId
78
+ * @returns {boolean}
79
+ */
80
+ function isInvited(user, tenantId) {
81
+ let allowedTenants = get(["config", "allowedTenants"], user);
82
+ if (!Array.isArray(allowedTenants)) {
83
+ return false;
84
+ }
85
+ return allowedTenants.some((item) => item && item.tenant && item.tenant.id === tenantId);
60
86
  }
61
87
 
62
88
  /**
@@ -75,20 +101,43 @@ Urac.prototype.init = function (cb) {
75
101
  }
76
102
  }
77
103
  else if (_self.id) {
78
- uracDriver.getRecord(_self.soajs, { id: _self.id.toString(), username: _self.username }, function (err, record) {
79
- if (record) {
80
- _self.userRecord = record;
81
- _self.resolveACL(() => {
82
- return cb(null, _self.userRecord);
83
- });
84
- } else {
85
- if (err && err.msg) {
86
- const code = err.code;
87
- err = new Error(err.msg);
88
- err.code = code;
104
+ let getRecord = () => {
105
+ uracDriver.getRecord(_self.soajs, { id: _self.id.toString(), username: _self.username }, function (err, record) {
106
+ if (record) {
107
+ _self.userRecord = record;
108
+ _self.resolveACL(() => {
109
+ return cb(null, _self.userRecord);
110
+ });
111
+ } else {
112
+ if (err && err.msg) {
113
+ const code = err.code;
114
+ err = new Error(err.msg);
115
+ err.code = code;
116
+ }
117
+ return cb(err, null);
89
118
  }
90
- return cb(err, null);
119
+ });
120
+ };
121
+ if (!_self.tenantAccess) {
122
+ return getRecord();
123
+ }
124
+ tenantAccess.resolve(_self.soajs, _self.bearerTokenUser, _self.tenantAccess, (result) => {
125
+ //NOTE: no access from the resolver falls back to the urac driver, it catches an invitation
126
+ // made after the token was issued and answers everyone else as before
127
+ if (!result) {
128
+ return getRecord();
91
129
  }
130
+ _self.userRecord = Object.assign({}, _self.bearerTokenUser, {
131
+ "tenant": {
132
+ "id": _self.soajs.tenant.id,
133
+ "code": _self.soajs.tenant.code
134
+ },
135
+ "groups": result.groups,
136
+ "groupsConfig": result.groupsConfig
137
+ });
138
+ _self.resolveACL(() => {
139
+ return cb(null, _self.userRecord);
140
+ });
92
141
  });
93
142
  } else {
94
143
  let error = new Error('oAuth userId is not available to pull URAC profile');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "soajs.controller",
3
3
  "description": "soajs multi tenant API gateway",
4
- "version": "4.3.17",
4
+ "version": "4.3.19",
5
5
  "author": {
6
6
  "name": "soajs team",
7
7
  "email": "team@soajs.org"
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ let doc = {
3
+ "_id": "5d7f8e43edf21900fed08cf7",
4
+ "name": "gateway",
5
+ "locked": false,
6
+ "plugged": true,
7
+ "shared": false,
8
+ "value": {
9
+ "idempotency": {
10
+ "model": "mongo",
11
+ "services": {
12
+ "idempotent": {
13
+ "enabled": true,
14
+ "ttl": 60000,
15
+ "apis": ["POST /ok", "POST /fail", "POST /bad", "POST /slow", "POST /throttled"]
16
+ }
17
+ }
18
+ },
19
+ "traffic": {
20
+ "throttling": {
21
+ "idempotent": {
22
+ "publicAPIStrategy": "idempotent_tight",
23
+ "apis": ["/throttled"]
24
+ }
25
+ }
26
+ }
27
+ },
28
+ "created": "DEV",
29
+ "author": "owner"
30
+ };
31
+
32
+ module.exports = doc;
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+ let doc = {
3
+ "_id": "5d7f8e43edf21900fed08cf6",
4
+ "name": "urac",
5
+ "locked": false,
6
+ "plugged": true,
7
+ "shared": false,
8
+ "value": {
9
+ "tenantAccessResolvers": {
10
+ "department": {
11
+ "service": "departments",
12
+ "version": "1",
13
+ "api": "/tenant/access",
14
+ "timeout": 1000
15
+ }
16
+ }
17
+ },
18
+ "created": "DEV",
19
+ "author": "owner"
20
+ };
21
+
22
+ module.exports = doc;
@@ -115,6 +115,15 @@ let env = {
115
115
  "accessTokenLifetime": 7200,
116
116
  "refreshTokenLifetime": 1209600
117
117
  },
118
+ "throttling": {
119
+ "idempotent_tight": {
120
+ "type": 1,
121
+ "window": 60000,
122
+ "limit": 1,
123
+ "retries": 0,
124
+ "delay": 100
125
+ }
126
+ },
118
127
  "ports": {
119
128
  "controller": 4000,
120
129
  "maintenanceInc": 1001,
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ let host = {
4
+ _id: "5da078e22357684b57effcf1",
5
+ env: "dev",
6
+ ip: "127.0.0.1",
7
+ name: "departments",
8
+ version: "1",
9
+ port: 4013
10
+ };
11
+
12
+ module.exports = host;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ let host = {
4
+ _id: "5da078e22357684b57effcf3",
5
+ env: "dev",
6
+ ip: "127.0.0.1",
7
+ name: "idempotent",
8
+ version: "1",
9
+ port: 4014
10
+ };
11
+
12
+ module.exports = host;
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ let service = {
4
+ _id: "5da078c92357684b57effcf0",
5
+ "type": "service",
6
+ name: "departments",
7
+ configuration: {
8
+ group: "SOAJS test",
9
+ port: 4013,
10
+ requestTimeout: 30,
11
+ requestTimeoutRenewal: 5
12
+ },
13
+ versions: [{
14
+ "version": "1",
15
+ maintenance: {
16
+ "port": {
17
+ "type": "maintenance"
18
+ },
19
+ "readiness": "/heartbeat"
20
+ },
21
+ oauth: true,
22
+ extKeyRequired: true,
23
+ urac: false,
24
+ urac_Profile: false,
25
+ urac_ACL: false,
26
+ provision_ACL: false,
27
+ apis: [
28
+ {
29
+ l: "Tenant access resolver",
30
+ v: "/tenant/access",
31
+ m: "post",
32
+ group: "Integration test"
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ };
38
+ module.exports = service;
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ let service = {
4
+ _id: "5da078c92357684b57effcf2",
5
+ "type": "service",
6
+ name: "idempotent",
7
+ configuration: {
8
+ group: "SOAJS test",
9
+ port: 4014,
10
+ requestTimeout: 30,
11
+ requestTimeoutRenewal: 5
12
+ },
13
+ versions: [{
14
+ "version": "1",
15
+ maintenance: {
16
+ "port": {
17
+ "type": "maintenance"
18
+ },
19
+ "readiness": "/heartbeat"
20
+ },
21
+ oauth: true,
22
+ extKeyRequired: true,
23
+ urac: true,
24
+ urac_Profile: false,
25
+ urac_ACL: false,
26
+ provision_ACL: false,
27
+ apis: [
28
+ { l: "Answers 200", v: "/ok", m: "post", group: "Integration test" },
29
+ { l: "Answers 503 the first time", v: "/fail", m: "post", group: "Integration test" },
30
+ { l: "Answers 400", v: "/bad", m: "post", group: "Integration test" },
31
+ { l: "Answers 200 after a delay", v: "/slow", m: "post", group: "Integration test" },
32
+ { l: "Answers 200, throttled", v: "/throttled", m: "post", group: "Integration test" }
33
+ ]
34
+ }
35
+ ]
36
+ };
37
+ module.exports = service;
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ let token = {
4
+ _id: "5cdc190fd52c82e0ddb1dcd6",
5
+ type: "accessToken",
6
+ token: "d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d901",
7
+ clientId: "5c0e74ba9acc3c5a84a51260",
8
+ user: {
9
+ _id: "5c8d0c505653de3985aa1001",
10
+ locked: true,
11
+ username: "dept_member",
12
+ firstName: "owner3'sèį",
13
+ lastName: "owner",
14
+ email: "me@localhost.com",
15
+ "ts": new Date().getTime(),
16
+ status: "active",
17
+ profile: {},
18
+ groups: [
19
+ "owner"
20
+ ],
21
+ config: {
22
+ packages: {},
23
+ keys: {},
24
+ allowedTenants: [
25
+ {
26
+ tenant: {
27
+ id: "THYME_tID",
28
+ code: "THYME_CODE",
29
+ pin: {
30
+ code: "5678",
31
+ allowed: true
32
+ }
33
+ },
34
+ groups: [
35
+ "waiter"
36
+ ]
37
+ },
38
+ {
39
+ tenant: {
40
+ id: "ELVIRA_tID",
41
+ code: "ELVIRA_CODE"
42
+ },
43
+ groups: [
44
+ "manager"
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ tenant: {
50
+ id: "5c0e74ba9acc3c5a84a51260",
51
+ code: "HOME",
52
+ pin: {
53
+ code: "1235",
54
+ allowed: true
55
+ }
56
+ },
57
+ lastLogin: new Date().getTime(),
58
+ groupsConfig: {
59
+ allowedPackages: {
60
+ DSBRD: [
61
+ "DSBRD_OWNER"
62
+ ]
63
+ },
64
+ allowedEnvironments: {}
65
+ },
66
+ loginMode: "urac",
67
+ id: "5c8d0c505653de3985aa1001"
68
+ },
69
+ env: "dev",
70
+ expires: new Date((new Date().getFullYear()) + 2, 0, 1)
71
+ };
72
+ module.exports = token;
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ let token = {
4
+ _id: "5cdc190fd52c82e0ddb1dcd7",
5
+ type: "accessToken",
6
+ token: "d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d902",
7
+ clientId: "5c0e74ba9acc3c5a84a51260",
8
+ user: {
9
+ _id: "5c8d0c505653de3985aa1002",
10
+ locked: true,
11
+ username: "dept_outsider",
12
+ firstName: "owner3'sèį",
13
+ lastName: "owner",
14
+ email: "me@localhost.com",
15
+ "ts": new Date().getTime(),
16
+ status: "active",
17
+ profile: {},
18
+ groups: [
19
+ "owner"
20
+ ],
21
+ config: {
22
+ packages: {},
23
+ keys: {},
24
+ allowedTenants: [
25
+ {
26
+ tenant: {
27
+ id: "THYME_tID",
28
+ code: "THYME_CODE",
29
+ pin: {
30
+ code: "5678",
31
+ allowed: true
32
+ }
33
+ },
34
+ groups: [
35
+ "waiter"
36
+ ]
37
+ },
38
+ {
39
+ tenant: {
40
+ id: "ELVIRA_tID",
41
+ code: "ELVIRA_CODE"
42
+ },
43
+ groups: [
44
+ "manager"
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ tenant: {
50
+ id: "5c0e74ba9acc3c5a84a51260",
51
+ code: "HOME",
52
+ pin: {
53
+ code: "1235",
54
+ allowed: true
55
+ }
56
+ },
57
+ lastLogin: new Date().getTime(),
58
+ groupsConfig: {
59
+ allowedPackages: {
60
+ DSBRD: [
61
+ "DSBRD_OWNER"
62
+ ]
63
+ },
64
+ allowedEnvironments: {}
65
+ },
66
+ loginMode: "urac",
67
+ id: "5c8d0c505653de3985aa1002"
68
+ },
69
+ env: "dev",
70
+ expires: new Date((new Date().getFullYear()) + 2, 0, 1)
71
+ };
72
+ module.exports = token;
@@ -9,6 +9,23 @@ let prod = {
9
9
  "scope": {
10
10
  "acl": {
11
11
  "dev": {
12
+ "idempotent": {
13
+ "1": {
14
+ "access": true,
15
+ "post": [
16
+ {
17
+ "group": "Idempotent group",
18
+ "apis": {
19
+ "/ok": {},
20
+ "/fail": {},
21
+ "/bad": {},
22
+ "/slow": {},
23
+ "/throttled": {}
24
+ }
25
+ }
26
+ ]
27
+ }
28
+ },
12
29
  "httpmethods": {
13
30
  "1": {
14
31
  "access": true,
@@ -200,6 +217,14 @@ let prod = {
200
217
  "locked": true,
201
218
  "acl": {
202
219
  "dev": {
220
+ "idempotent": [
221
+ {
222
+ "version": "1",
223
+ "post": [
224
+ "Idempotent group"
225
+ ]
226
+ }
227
+ ],
203
228
  "httpmethods": [
204
229
  {
205
230
  "version": "1",
@@ -5,6 +5,12 @@ let tenant = {
5
5
  "code": "DBTN",
6
6
  "locked": true,
7
7
  "name": "Console Tenant",
8
+ "profile": {
9
+ "access": {
10
+ "resolver": "department",
11
+ "subject": "dept_1"
12
+ }
13
+ },
8
14
  "description": "This is the tenant that holds the access rights and configuration for the console users with DSBRD_GUEST as Guest default package",
9
15
  "oauth": {
10
16
  "secret": "this is a secret",