summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/onap/clamp/authorization/AuthorizationController.java147
-rw-r--r--src/main/java/org/onap/clamp/util/PrincipalUtils.java82
-rw-r--r--src/main/resources/META-INF/resources/designer/index.html1
-rw-r--r--src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js12
-rw-r--r--src/main/resources/META-INF/resources/designer/scripts/CldsTemplateService.js67
-rw-r--r--src/main/resources/META-INF/resources/designer/scripts/GlobalPropertiesCtrl.js3
-rw-r--r--src/main/resources/application-noaaf.properties2
-rw-r--r--src/main/resources/application.properties2
-rw-r--r--src/main/resources/clds/camel/rest/clamp-api-v2.xml24
-rw-r--r--src/main/resources/clds/clds-users.json1
-rw-r--r--src/test/java/org/onap/clamp/clds/it/AuthorizationControllerItCase.java94
11 files changed, 344 insertions, 91 deletions
diff --git a/src/main/java/org/onap/clamp/authorization/AuthorizationController.java b/src/main/java/org/onap/clamp/authorization/AuthorizationController.java
new file mode 100644
index 000000000..206102758
--- /dev/null
+++ b/src/main/java/org/onap/clamp/authorization/AuthorizationController.java
@@ -0,0 +1,147 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights
+ * reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * ===================================================================
+ *
+ */
+
+package org.onap.clamp.authorization;
+
+import com.att.eelf.configuration.EELFLogger;
+import com.att.eelf.configuration.EELFManager;
+
+import java.util.Date;
+
+import javax.ws.rs.NotAuthorizedException;
+
+import org.apache.camel.Exchange;
+import org.onap.clamp.clds.config.ClampProperties;
+import org.onap.clamp.clds.service.SecureServiceBase;
+import org.onap.clamp.clds.service.SecureServicePermission;
+import org.onap.clamp.clds.util.LoggingUtils;
+import org.onap.clamp.util.PrincipalUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+/**
+ * Create CLDS Event.
+ */
+@Component
+public class AuthorizationController {
+
+ protected static final EELFLogger logger = EELFManager.getInstance().getLogger(SecureServiceBase.class);
+ protected static final EELFLogger auditLogger = EELFManager.getInstance().getMetricsLogger();
+ protected static final EELFLogger securityLogger = EELFManager.getInstance().getSecurityLogger();
+
+ // By default we'll set it to a default handler
+ @Autowired
+ private ClampProperties refProp;
+
+ private SecurityContext securityContext = SecurityContextHolder.getContext();
+ private final static String permPrefix = "security.permission.type.";
+ private final static String permInstance = "security.permission.instance";
+
+ public AuthorizationController() {
+ }
+ /**
+ * Insert event using process variables.
+ *
+ * @param camelExchange
+ * The Camel Exchange object containing the properties
+ * @param actionState
+ * The action state that is used instead of the one in exchange property
+ */
+
+ public void authorize (Exchange camelExchange, String typeVar, String instanceVar, String action) {
+ String type = refProp.getStringValue(permPrefix + typeVar);
+ String instance = refProp.getStringValue(permInstance);
+
+ if (null == type || type.isEmpty()) {
+ //authorization is turned off, since the permission is not defined
+ return;
+ }
+ if (null != instanceVar && !instanceVar.isEmpty()) {
+ instance = instanceVar;
+ }
+ String principalName = PrincipalUtils.getPrincipalName();
+ SecureServicePermission perm = SecureServicePermission.create(type, instance, action);
+ Date startTime = new Date();
+ LoggingUtils.setTargetContext("Clamp", "authorize");
+ LoggingUtils.setTimeContext(startTime, new Date());
+ securityLogger.debug("checking if {} has permission: {}", principalName, perm);
+ try {
+ isUserPermitted(perm);
+ } catch (NotAuthorizedException nae) {
+ String msg = principalName + " does not have permission: " + perm;
+ LoggingUtils.setErrorContext("100", "Authorization Error");
+ securityLogger.warn(msg);
+ throw new NotAuthorizedException(msg);
+ }
+ }
+
+ private boolean isUserPermitted(SecureServicePermission inPermission) {
+ boolean authorized = false;
+ String principalName = PrincipalUtils.getPrincipalName();
+ // check if the user has the permission key or the permission key with a
+ // combination of all instance and/or all action.
+ if (hasRole(inPermission.getKey())) {
+ auditLogger.info("{} authorized because user has permission with * for instance: {}", principalName, inPermission.getKey());
+ authorized = true;
+ // the rest of these don't seem to be required - isUserInRole method
+ // appears to take * as a wildcard
+ } else if (hasRole(inPermission.getKeyAllInstance())) {
+ auditLogger.info("{} authorized because user has permission with * for instance: {}", principalName, inPermission.getKey());
+ authorized = true;
+ } else if (hasRole(inPermission.getKeyAllInstanceAction())) {
+ auditLogger.info("{} authorized because user has permission with * for instance and * for action: {}", principalName, inPermission.getKey());
+ authorized = true;
+ } else if (hasRole(inPermission.getKeyAllAction())) {
+ auditLogger.info("{} authorized because user has permission with * for action: {}", principalName, inPermission.getKey());
+ authorized = true;
+ } else {
+ throw new NotAuthorizedException("");
+ }
+ return authorized;
+ }
+
+ public boolean isUserPermittedNoException(SecureServicePermission inPermission) {
+ try {
+ return isUserPermitted (inPermission);
+ } catch (NotAuthorizedException e) {
+ return false;
+ }
+ }
+
+ protected boolean hasRole(String role) {
+ Authentication authentication = PrincipalUtils.getSecurityContext().getAuthentication();
+ if (authentication == null) {
+ return false;
+ }
+ for (GrantedAuthority auth : authentication.getAuthorities()) {
+ if (role.equals(auth.getAuthority()))
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/org/onap/clamp/util/PrincipalUtils.java b/src/main/java/org/onap/clamp/util/PrincipalUtils.java
new file mode 100644
index 000000000..ec089834d
--- /dev/null
+++ b/src/main/java/org/onap/clamp/util/PrincipalUtils.java
@@ -0,0 +1,82 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights
+ * reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * Modifications copyright (c) 2018 Nokia
+ * ===================================================================
+ *
+ */
+
+package org.onap.clamp.util;
+
+import java.util.Date;
+
+import org.onap.clamp.clds.service.DefaultUserNameHandler;
+import org.onap.clamp.clds.service.UserNameHandler;
+import org.onap.clamp.clds.util.LoggingUtils;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class PrincipalUtils {
+ private static UserNameHandler userNameHandler = new DefaultUserNameHandler();
+ private static SecurityContext securityContext = SecurityContextHolder.getContext();
+
+ /**
+ * Get the Full name.
+ *
+ * @return
+ */
+ public static String getUserName() {
+ String name = userNameHandler.retrieveUserName(securityContext);
+ Date startTime = new Date();
+ LoggingUtils.setTargetContext("CLDS", "getUserName");
+ LoggingUtils.setTimeContext(startTime, new Date());
+ return name;
+ }
+
+ /**
+ * Get the userId from AAF/CSP.
+ *
+ * @return
+ */
+ public static String getUserId() {
+ return getUserName();
+ }
+
+ /**
+ * Get the principal name.
+ *
+ * @return
+ */
+ public static String getPrincipalName() {
+ String principal = ((UserDetails)securityContext.getAuthentication().getPrincipal()).getUsername();
+ String name = "Not found";
+ if (principal != null) {
+ name = principal;
+ }
+ return name;
+ }
+ public static void setSecurityContext(SecurityContext securityContext) {
+ PrincipalUtils.securityContext = securityContext;
+ }
+
+ public static SecurityContext getSecurityContext() {
+ return securityContext;
+ }
+}
diff --git a/src/main/resources/META-INF/resources/designer/index.html b/src/main/resources/META-INF/resources/designer/index.html
index e30d7245b..ec13e2a02 100644
--- a/src/main/resources/META-INF/resources/designer/index.html
+++ b/src/main/resources/META-INF/resources/designer/index.html
@@ -172,7 +172,6 @@
<script src="scripts/ExtraUserInfoCtrl.js"></script>
<script src="scripts/ExtraUserInfoService.js"></script>
<script src="scripts/saveConfirmationModalPopUpCtrl.js"></script>
- <script src="scripts/CldsTemplateService.js"></script>
<script src="scripts/GlobalPropertiesCtrl.js"></script>
<script src="scripts/AlertService.js"></script>
<script src="scripts/ToscaModelCtrl.js"></script>
diff --git a/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js b/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js
index a1625a936..0e3fce971 100644
--- a/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js
+++ b/src/main/resources/META-INF/resources/designer/scripts/CldsOpenModelCtrl.js
@@ -32,9 +32,8 @@ app
'cldsModelService',
'$location',
'dialogs',
-'cldsTemplateService',
function($scope, $rootScope, $modalInstance, $window, $uibModalInstance, cldsModelService, $location,
- dialogs, cldsTemplateService) {
+ dialogs) {
$scope.typeModel = 'template';
$scope.error = {
flag : false,
@@ -67,15 +66,6 @@ function($scope, $rootScope, $modalInstance, $window, $uibModalInstance, cldsMod
$scope.close();
}
}
- cldsTemplateService.getSavedTemplate().then(function(pars) {
- $scope.templateNamel = []
- for (var i = 0; i < pars.length; i++) {
- $scope.templateNamel.push(pars[i].value);
- }
- setTimeout(function() {
- setMultiSelect();
- }, 100);
- });
function contains(a, obj) {
var i = a && a.length > 0 ? a.length : 0;
while (i--) {
diff --git a/src/main/resources/META-INF/resources/designer/scripts/CldsTemplateService.js b/src/main/resources/META-INF/resources/designer/scripts/CldsTemplateService.js
deleted file mode 100644
index 4a0e7147c..000000000
--- a/src/main/resources/META-INF/resources/designer/scripts/CldsTemplateService.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/*-
- * ============LICENSE_START=======================================================
- * ONAP CLAMP
- * ================================================================================
- * Copyright (C) 2017-2018 AT&T Intellectual Property. All rights
- * reserved.
- * ================================================================================
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ============LICENSE_END============================================
- * ===================================================================
- *
- */
-
-app.service('cldsTemplateService', ['alertService', '$http', '$q', function (alertService, $http, $q) {
- this.getTemplate = function(templateName){
-
-
- var def = $q.defer();
- var sets = [];
-
- var svcUrl = "/restservices/clds/v1/cldsTempate/template/" + templateName;
-
- $http.get(svcUrl)
- .success(function(data){
-
- def.resolve(data);
-
- })
- .error(function(data){
-
- def.reject("Open Model not successful");
- });
-
- return def.promise;
- };
- this.getSavedTemplate=function(){
-
- var def = $q.defer();
- var sets = [];
-
- var svcUrl = "/restservices/clds/v1/cldsTempate/template-names";
-
- $http.get(svcUrl)
- .success(function(data){
-
- def.resolve(data);
-
- })
- .error(function(data){
-
- def.reject("Open Model not successful");
- });
-
- return def.promise;
- };
-
- }]);
diff --git a/src/main/resources/META-INF/resources/designer/scripts/GlobalPropertiesCtrl.js b/src/main/resources/META-INF/resources/designer/scripts/GlobalPropertiesCtrl.js
index 2ac959b45..e9ff49961 100644
--- a/src/main/resources/META-INF/resources/designer/scripts/GlobalPropertiesCtrl.js
+++ b/src/main/resources/META-INF/resources/designer/scripts/GlobalPropertiesCtrl.js
@@ -27,9 +27,8 @@ app.controller('GlobalPropertiesCtrl', [
'cldsModelService',
'$location',
'dialogs',
-'cldsTemplateService',
function($scope, $rootScope, $uibModalInstance, cldsModelService, $location,
- dialogs, cldsTemplateService) {
+ dialogs) {
$scope.$watch('name', function(newValue, oldValue) {
var el = getGlobalProperty();
diff --git a/src/main/resources/application-noaaf.properties b/src/main/resources/application-noaaf.properties
index 7dd0314a1..632856e92 100644
--- a/src/main/resources/application-noaaf.properties
+++ b/src/main/resources/application-noaaf.properties
@@ -208,7 +208,7 @@ clamp.config.dcae.header.requestId = X-ECOMP-RequestID
#Define user permission related parameters, the permission type can be changed but MUST be redefined in clds-users.properties in that case !
clamp.config.security.permission.type.cl=org.onap.clamp.clds.cl
clamp.config.security.permission.type.cl.manage=org.onap.clamp.clds.cl.manage
-clamp.config.security.permission.type.cl.event=org.onap.clds.cl.event
+clamp.config.security.permission.type.cl.event=org.onap.clamp.clds.cl.event
clamp.config.security.permission.type.filter.vf=org.onap.clamp.clds.filter.vf
clamp.config.security.permission.type.template=org.onap.clamp.clds.template
clamp.config.security.permission.type.tosca=org.onap.clamp.clds.tosca
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8859c4b32..91c02ef74 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -223,7 +223,7 @@ clamp.config.dcae.header.requestId = X-ECOMP-RequestID
#Define user permission related parameters, the permission type can be changed but MUST be redefined in clds-users.properties in that case !
clamp.config.security.permission.type.cl=org.onap.clamp.clds.cl
clamp.config.security.permission.type.cl.manage=org.onap.clamp.clds.cl.manage
-clamp.config.security.permission.type.cl.event=org.onap.clds.cl.event
+clamp.config.security.permission.type.cl.event=org.onap.clamp.clds.cl.event
clamp.config.security.permission.type.filter.vf=org.onap.clamp.clds.filter.vf
clamp.config.security.permission.type.template=org.onap.clamp.clds.template
clamp.config.security.permission.type.tosca=org.onap.clamp.clds.tosca
diff --git a/src/main/resources/clds/camel/rest/clamp-api-v2.xml b/src/main/resources/clds/camel/rest/clamp-api-v2.xml
index 0a72a0c12..0fd1250df 100644
--- a/src/main/resources/clds/camel/rest/clamp-api-v2.xml
+++ b/src/main/resources/clds/camel/rest/clamp-api-v2.xml
@@ -3,31 +3,39 @@
<get uri="/v2/loop/getAllNames"
outType="java.lang.String[]"
produces="application/json">
- <to
- uri="bean:org.onap.clamp.loop.LoopController?method=getLoopNames()" />
+ <route>
+ <to uri="bean:org.onap.clamp.authorization.AuthorizationController?method=authorize(*,'cl','','read')" />
+ <to uri="bean:org.onap.clamp.loop.LoopController?method=getLoopNames()" />
+ </route>
</get>
<get uri="/v2/loop/{loopName}"
outType="org.onap.clamp.loop.Loop"
produces="application/json">
- <to
- uri="bean:org.onap.clamp.loop.LoopController?method=getLoop(${header.loopName})" />
+ <route>
+ <to uri="bean:org.onap.clamp.authorization.AuthorizationController?method=authorize(*,'cl','','read')" />
+ <to uri="bean:org.onap.clamp.loop.LoopController?method=getLoop(${header.loopName})" />
+ </route>
</get>
<post uri="/v2/loop/updateOperationalPolicies/{loopName}"
type="com.google.gson.JsonArray"
consumes="application/json"
outType="org.onap.clamp.loop.Loop"
produces="application/json">
- <to
- uri="bean:org.onap.clamp.loop.LoopController?method=updateOperationalPolicies(${header.loopName},${body})" />
+ <route>
+ <to uri="bean:org.onap.clamp.authorization.AuthorizationController?method=authorize(*,'cl','','update')" />
+ <to uri="bean:org.onap.clamp.loop.LoopController?method=updateOperationalPolicies(${header.loopName},${body})" />
+ </route>
</post>
<post uri="/v2/loop/updateMicroservicePolicies/{loopName}"
type="com.google.gson.JsonArray"
consumes="application/json"
outType="org.onap.clamp.loop.Loop"
produces="application/json">
- <to
- uri="bean:org.onap.clamp.loop.LoopController?method=updateMicroservicePolicies(${header.loopName},${body})" />
+ <route>
+ <to uri="bean:org.onap.clamp.authorization.AuthorizationController?method=authorize(*,'cl','','update')" />
+ <to uri="bean:org.onap.clamp.loop.LoopController?method=updateMicroservicePolicies(${header.loopName},${body})" />
+ </route>
</post>
</rest>
</rests>
diff --git a/src/main/resources/clds/clds-users.json b/src/main/resources/clds/clds-users.json
index b4d73a29f..fe305980b 100644
--- a/src/main/resources/clds/clds-users.json
+++ b/src/main/resources/clds/clds-users.json
@@ -6,6 +6,7 @@
"org.onap.clamp.clds.cl|dev|read",
"org.onap.clamp.clds.cl|dev|update",
"org.onap.clamp.clds.cl.manage|dev|*",
+ "org.onap.clamp.clds.cl.event|dev|*",
"org.onap.clamp.clds.filter.vf|dev|*",
"org.onap.clamp.clds.template|dev|read",
"org.onap.clamp.clds.template|dev|update",
diff --git a/src/test/java/org/onap/clamp/clds/it/AuthorizationControllerItCase.java b/src/test/java/org/onap/clamp/clds/it/AuthorizationControllerItCase.java
new file mode 100644
index 000000000..477c71a0d
--- /dev/null
+++ b/src/test/java/org/onap/clamp/clds/it/AuthorizationControllerItCase.java
@@ -0,0 +1,94 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP CLAMP
+ * ================================================================================
+ * Copyright (C) 2019 AT&T Intellectual Property. All rights
+ * reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END============================================
+ * ===================================================================
+ *
+ */
+
+package org.onap.clamp.clds.it;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import com.att.eelf.configuration.EELFLogger;
+import com.att.eelf.configuration.EELFManager;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.onap.clamp.authorization.AuthorizationController;
+import org.onap.clamp.clds.service.SecureServicePermission;
+import org.onap.clamp.util.PrincipalUtils;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * Test CldsDAO calls through CldsModel and CldsEvent. This really test the DB
+ * and stored procedures.
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class AuthorizationControllerItCase {
+
+ protected static final EELFLogger logger = EELFManager.getInstance().getLogger(AuthorizationControllerItCase.class);
+ private Authentication authentication;
+ private List<GrantedAuthority> authList = new LinkedList<GrantedAuthority>();
+
+ /**
+ * Setup the variable before the tests execution.
+ *
+ * @throws IOException
+ * In case of issues when opening the files
+ */
+ @Before
+ public void setupBefore() throws IOException {
+ authList.add(new SimpleGrantedAuthority("permission-type-cl-manage|dev|*"));
+ authList.add(new SimpleGrantedAuthority("permission-type-cl|dev|read"));
+ authList.add(new SimpleGrantedAuthority("permission-type-cl|dev|update"));
+ authList.add(new SimpleGrantedAuthority("permission-type-template|dev|read"));
+ authList.add(new SimpleGrantedAuthority("permission-type-template|dev|update"));
+ authList.add(new SimpleGrantedAuthority("permission-type-filter-vf|dev|*"));
+ authList.add(new SimpleGrantedAuthority("permission-type-cl-event|dev|*"));
+
+ authentication = new UsernamePasswordAuthenticationToken(new User("admin", "", authList), "", authList);
+ }
+
+ @Test
+ public void testIsUserPermittedNoException() {
+ SecurityContext securityContext = Mockito.mock(SecurityContext.class);
+ Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
+ PrincipalUtils.setSecurityContext(securityContext);
+
+ AuthorizationController auth = new AuthorizationController ();
+ assertTrue(auth.isUserPermittedNoException(new SecureServicePermission("permission-type-cl","dev","read")));
+ assertTrue(auth.isUserPermittedNoException(new SecureServicePermission("permission-type-cl-manage","dev","DEPLOY")));
+ assertTrue(auth.isUserPermittedNoException(new SecureServicePermission("permission-type-filter-vf","dev","12345-55555-55555-5555")));
+ assertFalse(auth.isUserPermittedNoException(new SecureServicePermission("permission-type-cl","test","read")));
+ }
+}