summaryrefslogtreecommitdiffstats
path: root/controller-server
diff options
context:
space:
mode:
authorØyvind Grønnesby <oyving@verizonmedia.com>2019-10-09 09:18:37 +0200
committerGitHub <noreply@github.com>2019-10-09 09:18:37 +0200
commitf5a5dca146d8161462c144f31bfb4a91b8a5167a (patch)
treecffa686fd83ddb9ebf73f59fcc252fba52d8c268 /controller-server
parent3311d0878eaa98062a805d910f226f263661df87 (diff)
parent8ce9c7a0b9d90dfb1e8a64f5c623167f0062c3ca (diff)
Merge pull request #10921 from vespa-engine/ogronnesby/simplified-roles
Introduce simplified roles without removing old ones
Diffstat (limited to 'controller-server')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java88
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json65
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json40
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json2
9 files changed, 83 insertions, 140 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
index 7ad2e03ef1d..a41103453eb 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
@@ -20,13 +20,10 @@ import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.yolean.Exceptions;
-import java.security.Principal;
import java.security.PublicKey;
import java.util.Base64;
-import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
-import java.util.function.Function;
import java.util.logging.Logger;
import static java.nio.charset.StandardCharsets.UTF_8;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 77622df4c4a..59cfc188ebe 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -192,12 +192,11 @@ public class UserApiHandler extends LoggingRequestHandler {
UserId user = new UserId(require("user", Inspector::asString, requestObject));
Role role = Roles.toRole(TenantName.from(tenantName), roleName);
- if ( role.definition() == RoleDefinition.tenantOwner
+ if ( role.definition() == RoleDefinition.administrator
&& Set.of(user.value()).equals(users.listUsers(role).stream().map(User::email).collect(Collectors.toSet())))
- throw new IllegalArgumentException("Can't remove the last owner of a tenant.");
+ throw new IllegalArgumentException("Can't remove the last administrator of a tenant.");
- // TODO jonmv: Change to developer role, when this exists.
- if (role.definition().equals(RoleDefinition.tenantOperator))
+ if (role.definition().equals(RoleDefinition.developer))
controller.tenants().lockIfPresent(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
PublicKey key = tenant.get().developerKeys().inverse().get(new SimplePrincipal(user.value()));
if (key != null)
@@ -235,6 +234,10 @@ public class UserApiHandler extends LoggingRequestHandler {
case applicationOperator: return "applicationOperator";
case applicationDeveloper: return "applicationDeveloper";
case applicationReader: return "applicationReader";
+ case administrator: return "administrator";
+ case developer: return "developer";
+ case reader: return "reader";
+ case headless: return "headless";
default: throw new IllegalArgumentException("Unexpected role type '" + role.definition() + "'.");
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
index a88e38e5f89..363dc348ad3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
@@ -36,9 +36,14 @@ public class CloudAccessControl implements AccessControl {
CloudTenantSpec spec = (CloudTenantSpec) tenantSpec;
CloudTenant tenant = CloudTenant.create(spec.tenant(), defaultBillingInfo);
- for (Role role : Roles.tenantRoles(spec.tenant()))
+ for (Role role : Roles.tenantRoles(spec.tenant())) {
userManagement.createRole(role);
- userManagement.addUsers(Role.tenantOwner(spec.tenant()), List.of(new UserId(credentials.user().getName())));
+ }
+
+ var userId = List.of(new UserId(credentials.user().getName()));
+ userManagement.addUsers(Role.administrator(spec.tenant()), userId);
+ userManagement.addUsers(Role.developer(spec.tenant()), userId);
+ userManagement.addUsers(Role.reader(spec.tenant()), userId);
return tenant;
}
@@ -60,7 +65,6 @@ public class CloudAccessControl implements AccessControl {
public void createApplication(TenantAndApplicationId id, Credentials credentials) {
for (Role role : Roles.applicationRoles(id.tenant(), id.application()))
userManagement.createRole(role);
- userManagement.addUsers(Role.applicationAdmin(id.tenant(), id.application()), List.of(new UserId(credentials.user().getName())));
}
@Override
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
index b1f5f33b960..d98558d53dc 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
@@ -64,7 +64,7 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a tenant is available to operators.
tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
.roles(operator)
- .user("owner@tenant")
+ .user("administrator@tenant")
.data("{\"token\":\"hello\"}"),
new File("tenant-without-applications.json"));
@@ -79,128 +79,116 @@ public class UserApiTest extends ControllerContainerCloudTest {
// POST a hosted operator role is not allowed.
tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.tenantOwner(id.tenant())))
+ .roles(Set.of(Role.administrator(id.tenant())))
.data("{\"user\":\"evil@evil\",\"roleName\":\"hostedOperator\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'hostedOperator'.\"}", 400);
- // POST a tenant operator is available to the tenant owner.
+ // POST a tenant developer is available to the tenant owner.
tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.tenantOwner(id.tenant())))
- .data("{\"user\":\"operator@tenant\",\"roleName\":\"tenantOperator\"}"),
- "{\"message\":\"user 'operator@tenant' is now a member of role 'tenantOperator' of 'my-tenant'\"}");
+ .roles(Set.of(Role.administrator(id.tenant())))
+ .data("{\"user\":\"developer@tenant\",\"roleName\":\"developer\"}"),
+ "{\"message\":\"user 'developer@tenant' is now a member of role 'developer' of 'my-tenant'\"}");
- // POST a tenant admin is not available to a tenant operator.
+ // POST a tenant admin is not available to a tenant developer.
tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.tenantOperator(id.tenant())))
- .data("{\"user\":\"admin@tenant\",\"roleName\":\"tenantAdmin\"}"),
+ .roles(Set.of(Role.developer(id.tenant())))
+ .data("{\"user\":\"developer@tenant\",\"roleName\":\"administrator\"}"),
accessDenied, 403);
- // POST an application admin for a non-existent application fails.
+ // POST a headless for a non-existent application fails.
tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", POST)
- .roles(Set.of(Role.tenantOwner(TenantName.from("my-tenant"))))
- .data("{\"user\":\"admin@app\",\"roleName\":\"applicationAdmin\"}"),
+ .roles(Set.of(Role.administrator(TenantName.from("my-tenant"))))
+ .data("{\"user\":\"headless@app\",\"roleName\":\"headless\"}"),
"{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"NullPointerException\"}", 500);
- // POST an application is allowed for a tenant operator.
+ // POST an application is allowed for a tenant developer.
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", POST)
- .user("operator@tenant")
- .roles(Set.of(Role.tenantOperator(id.tenant()))),
+ .user("developer@tenant")
+ .roles(Set.of(Role.developer(id.tenant()))),
new File("application-created.json"));
// POST an application is not allowed under a different tenant.
tester.assertResponse(request("/application/v4/tenant/other-tenant/application/my-app", POST)
- .roles(Set.of(Role.tenantOperator(id.tenant()))),
+ .roles(Set.of(Role.administrator(id.tenant()))),
accessDenied, 403);
- // POST an application role is allowed for a tenant admin.
- tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", POST)
- .roles(Set.of(Role.tenantAdmin(id.tenant())))
- .data("{\"user\":\"reader@app\",\"roleName\":\"applicationReader\"}"),
- "{\"message\":\"user 'reader@app' is now a member of role 'applicationReader' of 'my-app' owned by 'my-tenant'\"}");
-
// POST a tenant role is not allowed to an application.
tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", POST)
.roles(Set.of(Role.hostedOperator()))
- .data("{\"user\":\"reader@app\",\"roleName\":\"tenantOperator\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'tenantOperator'.\"}", 400);
+ .data("{\"user\":\"developer@app\",\"roleName\":\"developer\"}"),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'developer'.\"}", 400);
- // GET tenant role information is available to application readers.
+ // GET tenant role information is available to readers.
tester.assertResponse(request("/user/v1/tenant/my-tenant")
- .roles(Set.of(Role.applicationReader(id.tenant(), id.application()))),
+ .roles(Set.of(Role.reader(id.tenant()))),
new File("tenant-roles.json"));
- // GET application role information is available to tenant operators.
+ // GET application role information is available to tenant administrators.
tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app")
- .roles(Set.of(Role.tenantOperator(id.tenant()))),
+ .roles(Set.of(Role.administrator(id.tenant()))),
new File("application-roles.json"));
// GET application role information is available also under the /api prefix.
tester.assertResponse(request("/api/user/v1/tenant/my-tenant/application/my-app")
- .roles(Set.of(Role.tenantOperator(id.tenant()))),
+ .roles(Set.of(Role.administrator(id.tenant()))),
new File("application-roles.json"));
// POST a pem deploy key
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
- .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
new File("first-deploy-key.json"));
// POST a pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
.user("joe@dev")
- .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
new File("first-developer-key.json"));
// POST the same pem developer key for a different user is forbidden
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
.user("operator@tenant")
- .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key "+ quotedPemPublicKey + " is already owned by joe@dev\"}",
400);
- // PATCH in a different pem developer key
+ // POST in a different pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .user("operator@tenant")
- .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .user("developer@tenant")
+ .roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + otherPemPublicKey + "\"}"),
new File("both-developer-keys.json"));
// GET tenant information with keys
tester.assertResponse(request("/application/v4/tenant/my-tenant/")
- .roles(Set.of(Role.applicationReader(id.tenant(), id.application()))),
+ .roles(Set.of(Role.reader(id.tenant()))),
new File("tenant-with-keys.json"));
// DELETE a pem developer key
tester.assertResponse(request("/application/v4/tenant/my-tenant/key", DELETE)
- .roles(Set.of(Role.tenantOperator(id.tenant())))
+ .roles(Set.of(Role.developer(id.tenant())))
.data("{\"key\":\"" + pemPublicKey + "\"}"),
new File("second-developer-key.json"));
- // DELETE an application role is allowed for an application admin.
- tester.assertResponse(request("/user/v1/tenant/my-tenant/application/my-app", DELETE)
- .roles(Set.of(Role.applicationAdmin(id.tenant(), id.application())))
- .data("{\"user\":\"operator@tenant\",\"roleName\":\"applicationAdmin\"}"),
- "{\"message\":\"user 'operator@tenant' is no longer a member of role 'applicationAdmin' of 'my-app' owned by 'my-tenant'\"}");
-
- // DELETE an application is available to application admins.
+ // DELETE an application is available to developers.
tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", DELETE)
- .roles(Set.of(Role.applicationAdmin(id.tenant(), id.application()))),
+ .roles(Set.of(Role.developer(id.tenant()))),
"{\"message\":\"Deleted application my-tenant.my-app\"}");
// DELETE a tenant role is available to tenant admins.
// DELETE the developer role clears any developer key.
tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
- .roles(Set.of(Role.tenantAdmin(id.tenant())))
- .data("{\"user\":\"operator@tenant\",\"roleName\":\"tenantOperator\"}"),
- "{\"message\":\"user 'operator@tenant' is no longer a member of role 'tenantOperator' of 'my-tenant'\"}");
+ .roles(Set.of(Role.administrator(id.tenant())))
+ .data("{\"user\":\"developer@tenant\",\"roleName\":\"developer\"}"),
+ "{\"message\":\"user 'developer@tenant' is no longer a member of role 'developer' of 'my-tenant'\"}");
// DELETE the last tenant owner is not allowed.
tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
.roles(operator)
- .data("{\"user\":\"owner@tenant\",\"roleName\":\"tenantOwner\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Can't remove the last owner of a tenant.\"}", 400);
+ .data("{\"user\":\"administrator@tenant\",\"roleName\":\"administrator\"}"),
+ "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Can't remove the last administrator of a tenant.\"}", 400);
// DELETE the tenant is available to the tenant owner.
tester.assertResponse(request("/application/v4/tenant/my-tenant", DELETE)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json
index e23ab918135..e05156e3eef 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-roles.json
@@ -2,77 +2,28 @@
"tenant": "my-tenant",
"application": "my-app",
"roleNames": [
- "applicationAdmin",
- "applicationOperator",
- "applicationDeveloper",
- "applicationReader"
+ "headless"
],
"users": [
{
- "name": "owner@tenant",
- "email":"owner@tenant",
+ "name": "administrator@tenant",
+ "email": "administrator@tenant",
"roles": {
- "applicationAdmin": {
+ "headless": {
"explicit": false,
- "implied": true
- },
- "applicationOperator": {
- "explicit": false,
- "implied": true
- },
- "applicationDeveloper": {
- "explicit": false,
- "implied": true
- },
- "applicationReader": {
- "explicit": false,
- "implied": true
- }
- }
- },
- {
- "name": "operator@tenant",
- "email":"operator@tenant",
- "roles": {
- "applicationAdmin": {
- "explicit": true,
"implied": false
- },
- "applicationOperator": {
- "explicit": false,
- "implied": true
- },
- "applicationDeveloper": {
- "explicit": false,
- "implied": true
- },
- "applicationReader": {
- "explicit": false,
- "implied": true
}
}
},
{
- "name": "reader@app",
- "email":"reader@app",
+ "name": "developer@tenant",
+ "email": "developer@tenant",
"roles": {
- "applicationAdmin": {
- "explicit": false,
- "implied": false
- },
- "applicationOperator": {
- "explicit": false,
- "implied": false
- },
- "applicationDeveloper": {
+ "headless": {
"explicit": false,
"implied": false
- },
- "applicationReader": {
- "explicit": true,
- "implied": false
}
}
}
]
-}
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
index 2ff1c29fe29..bc49135a1db 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
@@ -6,7 +6,7 @@
},
{
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "operator@tenant"
+ "user": "developer@tenant"
}
]
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
index f7d90f31116..64098a775a1 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
@@ -2,7 +2,7 @@
"keys": [
{
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "operator@tenant"
+ "user": "developer@tenant"
}
]
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json
index fa528fe2ab7..61c1c94d4ca 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json
@@ -1,46 +1,46 @@
{
"tenant": "my-tenant",
"roleNames": [
- "tenantOwner",
- "tenantAdmin",
- "tenantOperator"
+ "administrator",
+ "developer",
+ "reader"
],
"users": [
{
- "name": "owner@tenant",
- "email":"owner@tenant",
+ "name": "administrator@tenant",
+ "email": "administrator@tenant",
"roles": {
- "tenantOwner": {
+ "administrator": {
"explicit": true,
"implied": false
},
- "tenantAdmin": {
- "explicit": false,
- "implied": true
+ "developer": {
+ "explicit": true,
+ "implied": false
},
- "tenantOperator": {
- "explicit": false,
- "implied": true
+ "reader": {
+ "explicit": true,
+ "implied": false
}
}
},
{
- "name": "operator@tenant",
- "email":"operator@tenant",
+ "name": "developer@tenant",
+ "email": "developer@tenant",
"roles": {
- "tenantOwner": {
+ "administrator": {
"explicit": false,
"implied": false
},
- "tenantAdmin": {
- "explicit": false,
+ "developer": {
+ "explicit": true,
"implied": false
},
- "tenantOperator": {
- "explicit": true,
+ "reader": {
+ "explicit": false,
"implied": false
}
}
}
]
-}
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
index b7970a48963..24ac6633f6c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
@@ -8,7 +8,7 @@
},
{
"key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "operator@tenant"
+ "user": "developer@tenant"
}],
"applications": []
}