You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by sm...@apache.org on 2015/08/17 06:09:09 UTC
[07/10] airavata git commit: added PAP client in Airavata Server,
which publishes and enables the default XACML authorization policy in
the XACML authorization server,
at the airavata server startup - if the security is enabled.
added PAP client in Airavata Server, which publishes and enables the default XACML authorization policy in the XACML authorization server, at the airavata server startup - if the security is enabled.
Project: http://git-wip-us.apache.org/repos/asf/airavata/repo
Commit: http://git-wip-us.apache.org/repos/asf/airavata/commit/59f4acda
Tree: http://git-wip-us.apache.org/repos/asf/airavata/tree/59f4acda
Diff: http://git-wip-us.apache.org/repos/asf/airavata/diff/59f4acda
Branch: refs/heads/master
Commit: 59f4acda5c600cb7c11a645fba1bacb4bad27e16
Parents: c365260
Author: hasinitg <ha...@gmail.com>
Authored: Sat Aug 8 01:21:08 2015 +0530
Committer: hasinitg <ha...@gmail.com>
Committed: Sat Aug 8 01:21:08 2015 +0530
----------------------------------------------------------------------
airavata-api/airavata-api-server/pom.xml | 5 +
.../airavata/api/server/AiravataAPIServer.java | 10 ++
.../security/AiravataSecurityManager.java | 13 ++
.../DefaultAiravataSecurityManager.java | 56 ++++++++-
.../api/server/security/DefaultPAPClient.java | 126 +++++++++++++++++++
.../api/server/security/DefaultXACMLPEP.java | 3 +-
.../apache/airavata/common/utils/Constants.java | 2 +
.../airavata/common/utils/ServerSettings.java | 6 +-
.../resources/airavata-default-xacml-policy.xml | 2 +-
.../main/resources/airavata-server.properties | 1 +
10 files changed, 219 insertions(+), 5 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/pom.xml
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/pom.xml b/airavata-api/airavata-api-server/pom.xml
index 543bbaa..e78ff9d 100644
--- a/airavata-api/airavata-api-server/pom.xml
+++ b/airavata-api/airavata-api-server/pom.xml
@@ -113,6 +113,11 @@
<version>4.2.1</version>
</dependency>
<dependency>
+ <groupId>org.wso2.carbon</groupId>
+ <artifactId>org.wso2.carbon.identity.entitlement.common</artifactId>
+ <version>4.2.1</version>
+ </dependency>
+ <dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>4.0</version>
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/AiravataAPIServer.java
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/AiravataAPIServer.java b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/AiravataAPIServer.java
index 1b336e1..c06cd39 100644
--- a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/AiravataAPIServer.java
+++ b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/AiravataAPIServer.java
@@ -27,6 +27,8 @@ import java.net.InetAddress;
import org.apache.airavata.api.Airavata;
import org.apache.airavata.api.server.handler.AiravataServerHandler;
+import org.apache.airavata.api.server.security.AiravataSecurityManager;
+import org.apache.airavata.api.server.security.SecurityManagerFactory;
import org.apache.airavata.api.server.security.SecurityModule;
import org.apache.airavata.api.server.util.AppCatalogInitUtil;
import org.apache.airavata.api.server.util.Constants;
@@ -38,6 +40,7 @@ import org.apache.airavata.common.utils.IServer;
import org.apache.airavata.common.utils.ServerSettings;
import org.apache.airavata.model.error.AiravataErrorType;
import org.apache.airavata.model.error.AiravataSystemException;
+import org.apache.airavata.security.AiravataSecurityException;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;
@@ -145,6 +148,10 @@ public class AiravataAPIServer implements IServer{
}.start();
logger.info("Airavata API server starter over TLS on Port: " + ServerSettings.getTLSServerPort());
}
+ //perform any security related initialization at the server startup, according to the security manager being used.
+ AiravataSecurityManager securityManager = SecurityManagerFactory.getSecurityManager();
+ securityManager.initializeSecurityInfra();
+
} catch (TTransportException e) {
logger.error(e.getMessage());
setStatus(ServerStatus.FAILED);
@@ -156,6 +163,9 @@ public class AiravataAPIServer implements IServer{
} catch (UnknownHostException e) {
logger.error(e.getMessage(), e);
throw new AiravataSystemException(AiravataErrorType.INTERNAL_ERROR);
+ } catch (AiravataSecurityException e) {
+ logger.error(e.getMessage(), e);
+ throw new AiravataSystemException(AiravataErrorType.INTERNAL_ERROR);
}
}
public static void main(String[] args) {
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/AiravataSecurityManager.java
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/AiravataSecurityManager.java b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/AiravataSecurityManager.java
index 37c348c..9245576 100644
--- a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/AiravataSecurityManager.java
+++ b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/AiravataSecurityManager.java
@@ -26,5 +26,18 @@ import org.apache.airavata.security.AiravataSecurityException;
import java.util.Map;
public interface AiravataSecurityManager {
+ /**
+ * Implement this method in your SecurityManager to perform necessary initializations at the server startup.
+ * @throws AiravataSecurityException
+ */
+ public void initializeSecurityInfra() throws AiravataSecurityException;
+
+ /**
+ * Implement this method with the user authentication/authorization logic in your SecurityManager.
+ * @param authzToken : this includes OAuth token and user's claims
+ * @param metaData : this includes other meta data needed for security enforcements.
+ * @return
+ * @throws AiravataSecurityException
+ */
public boolean isUserAuthorized(AuthzToken authzToken, Map<String, String> metaData) throws AiravataSecurityException;
}
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultAiravataSecurityManager.java
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultAiravataSecurityManager.java b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultAiravataSecurityManager.java
index 6230310..532f9f6 100644
--- a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultAiravataSecurityManager.java
+++ b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultAiravataSecurityManager.java
@@ -21,6 +21,7 @@
package org.apache.airavata.api.server.security;
import org.apache.airavata.common.exception.ApplicationSettingsException;
+import org.apache.airavata.common.utils.Constants;
import org.apache.airavata.common.utils.ServerSettings;
import org.apache.airavata.model.security.AuthzToken;
import org.apache.airavata.security.AiravataSecurityException;
@@ -32,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationResponseDTO;
+import java.io.*;
import java.util.Map;
/**
@@ -40,6 +42,56 @@ import java.util.Map;
public class DefaultAiravataSecurityManager implements AiravataSecurityManager {
private final static Logger logger = LoggerFactory.getLogger(DefaultAiravataSecurityManager.class);
+ @Override
+ public void initializeSecurityInfra() throws AiravataSecurityException {
+ /* in the default security manager, this method checks if the xacml authorization policy is published,
+ * and if not, publish the policy to the PDP (of WSO2 Identity Server)
+ */
+ try {
+ if (ServerSettings.isAPISecured()) {
+
+ ConfigurationContext configContext =
+ ConfigurationContextFactory.createConfigurationContextFromFileSystem(null, null);
+ //initialize SSL context with the trust store that contains the public cert of WSO2 Identity Server.
+ TrustStoreManager trustStoreManager = new TrustStoreManager();
+ trustStoreManager.initializeTrustStoreManager(ServerSettings.getTrustStorePath(),
+ ServerSettings.getTrustStorePassword());
+ DefaultPAPClient PAPClient = new DefaultPAPClient(ServerSettings.getRemoteAuthzServerUrl(),
+ ServerSettings.getAdminUsername(), ServerSettings.getAdminPassword(), configContext);
+ boolean policyAdded = PAPClient.isPolicyAdded(ServerSettings.getAuthorizationPoliyName());
+ if (policyAdded) {
+ logger.info("Authorization policy is already added in the authorization server.");
+ } else {
+ //read the policy as a string
+ BufferedReader bufferedReader = new BufferedReader(new FileReader(new File(
+ ServerSettings.getAuthorizationPoliyName() + ".xml")));
+ String line;
+ StringBuilder stringBuilder = new StringBuilder();
+ while ((line = bufferedReader.readLine()) != null) {
+ stringBuilder.append(line);
+ }
+ //publish the policy and enable it in a separate thread
+ PAPClient.addPolicy(stringBuilder.toString());
+ }
+ }
+
+ } catch (AxisFault axisFault) {
+ logger.error(axisFault.getMessage(), axisFault);
+ throw new AiravataSecurityException("Error in initializing the configuration context for creating the " +
+ "PAP client.");
+ } catch (ApplicationSettingsException e) {
+ logger.error(e.getMessage(), e);
+ throw new AiravataSecurityException("Error in reading configuration when creating the PAP client.");
+ } catch (FileNotFoundException e) {
+ logger.error(e.getMessage(), e);
+ throw new AiravataSecurityException("Error in reading authorization policy.");
+ } catch (IOException e) {
+ logger.error(e.getMessage(), e);
+ throw new AiravataSecurityException("Error in reading the authorization policy.");
+ }
+
+ }
+
public boolean isUserAuthorized(AuthzToken authzToken, Map<String, String> metaData) throws AiravataSecurityException {
try {
ConfigurationContext configContext =
@@ -50,13 +102,13 @@ public class DefaultAiravataSecurityManager implements AiravataSecurityManager {
trustStoreManager.initializeTrustStoreManager(ServerSettings.getTrustStorePath(),
ServerSettings.getTrustStorePassword());
- DefaultOAuthClient oauthClient = new DefaultOAuthClient(ServerSettings.getRemoteOauthServerUrl(),
+ DefaultOAuthClient oauthClient = new DefaultOAuthClient(ServerSettings.getRemoteAuthzServerUrl(),
ServerSettings.getAdminUsername(), ServerSettings.getAdminPassword(), configContext);
OAuth2TokenValidationResponseDTO validationResponse = oauthClient.validateAccessToken(
authzToken.getAccessToken());
boolean isOAuthTokenValid = validationResponse.getValid();
//if XACML based authorization is enabled, check for role based authorization for the API invocation
- DefaultXACMLPEP entitlementClient = new DefaultXACMLPEP(ServerSettings.getRemoteOauthServerUrl(),
+ DefaultXACMLPEP entitlementClient = new DefaultXACMLPEP(ServerSettings.getRemoteAuthzServerUrl(),
ServerSettings.getAdminUsername(), ServerSettings.getAdminPassword(), configContext);
boolean authorizationDecision = entitlementClient.getAuthorizationDecision(authzToken, metaData);
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultPAPClient.java
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultPAPClient.java b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultPAPClient.java
new file mode 100644
index 0000000..b75129c
--- /dev/null
+++ b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultPAPClient.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+package org.apache.airavata.api.server.security;
+
+import com.sun.corba.se.spi.activation.Server;
+import org.apache.airavata.common.exception.ApplicationSettingsException;
+import org.apache.airavata.common.utils.ServerSettings;
+import org.apache.airavata.security.AiravataSecurityException;
+import org.apache.axis2.AxisFault;
+import org.apache.axis2.context.ConfigurationContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wso2.carbon.identity.entitlement.stub.EntitlementPolicyAdminServiceStub;
+import org.wso2.carbon.identity.entitlement.stub.dto.PaginatedStatusHolder;
+import org.wso2.carbon.identity.entitlement.stub.dto.PolicyDTO;
+import org.wso2.carbon.identity.entitlement.stub.dto.StatusHolder;
+import org.wso2.carbon.identity.entitlement.common.EntitlementConstants;
+import org.wso2.carbon.identity.entitlement.stub.EntitlementPolicyAdminServiceEntitlementException;
+import org.wso2.carbon.utils.CarbonUtils;
+
+import java.rmi.RemoteException;
+
+/**
+ * This publishes the airavata-default-xacml-policy.xml to the PDP via PAP API (of WSO2 Identity Server)
+ */
+public class DefaultPAPClient {
+
+ private final static Logger logger = LoggerFactory.getLogger(DefaultPAPClient.class);
+ private EntitlementPolicyAdminServiceStub entitlementPolicyAdminServiceStub;
+
+ public DefaultPAPClient(String auhorizationServerURL, String username, String password,
+ ConfigurationContext configCtx) throws AiravataSecurityException {
+ try {
+
+ String PDPURL = auhorizationServerURL + "EntitlementPolicyAdminService";
+ entitlementPolicyAdminServiceStub = new EntitlementPolicyAdminServiceStub(configCtx, PDPURL);
+ CarbonUtils.setBasicAccessSecurityHeaders(username, password, true,
+ entitlementPolicyAdminServiceStub._getServiceClient());
+ } catch (AxisFault e) {
+ logger.error(e.getMessage(), e);
+ throw new AiravataSecurityException("Error initializing XACML PEP client.");
+ }
+
+ }
+
+ public boolean isPolicyAdded(String policyName) {
+ try {
+ PolicyDTO policyDTO = entitlementPolicyAdminServiceStub.getPolicy(policyName, false);
+ } catch (RemoteException e) {
+ logger.debug("Error in retrieving the policy.", e);
+ return false;
+ } catch (EntitlementPolicyAdminServiceEntitlementException e) {
+ logger.debug("Error in retrieving the policy.", e);
+ return false;
+ }
+ return true;
+ }
+
+ public void addPolicy(String policy) throws AiravataSecurityException {
+ new Thread() {
+ public void run() {
+ try {
+ PolicyDTO policyDTO = new PolicyDTO();
+ policyDTO.setPolicy(policy);
+ entitlementPolicyAdminServiceStub.addPolicy(policyDTO);
+ entitlementPolicyAdminServiceStub.publishToPDP(new String[]{ServerSettings.getAuthorizationPoliyName()},
+ EntitlementConstants.PolicyPublish.ACTION_CREATE, null, false, 0);
+
+ //Since policy publishing happens asynchronously, we need to retrieve the status and verify.
+ Thread.sleep(2000);
+ PaginatedStatusHolder paginatedStatusHolder = entitlementPolicyAdminServiceStub.
+ getStatusData(EntitlementConstants.Status.ABOUT_POLICY, ServerSettings.getAuthorizationPoliyName(),
+ EntitlementConstants.StatusTypes.PUBLISH_POLICY, "*", 1);
+ StatusHolder statusHolder = paginatedStatusHolder.getStatusHolders()[0];
+ if (statusHolder.getSuccess() && EntitlementConstants.PolicyPublish.ACTION_CREATE.equals(statusHolder.getTargetAction())) {
+ logger.info("Authorization policy is published successfully.");
+ } else {
+ throw new AiravataSecurityException("Failed to publish the authorization policy.");
+ }
+
+ //enable the published policy
+ entitlementPolicyAdminServiceStub.enableDisablePolicy(ServerSettings.getAuthorizationPoliyName(), true);
+ //Since policy enabling happens asynchronously, we need to retrieve the status and verify.
+ Thread.sleep(2000);
+ paginatedStatusHolder = entitlementPolicyAdminServiceStub.
+ getStatusData(EntitlementConstants.Status.ABOUT_POLICY, ServerSettings.getAuthorizationPoliyName(),
+ EntitlementConstants.StatusTypes.PUBLISH_POLICY, "*", 1);
+ statusHolder = paginatedStatusHolder.getStatusHolders()[0];
+ if (statusHolder.getSuccess() && EntitlementConstants.PolicyPublish.ACTION_ENABLE.equals(statusHolder.getTargetAction())) {
+ logger.info("Authorization policy is enabled successfully.");
+ } else {
+ throw new AiravataSecurityException("Failed to enable the authorization policy.");
+ }
+ } catch (RemoteException e) {
+ logger.error(e.getMessage(), e);
+ } catch (InterruptedException e) {
+ logger.error(e.getMessage(), e);
+ } catch (ApplicationSettingsException e) {
+ logger.error(e.getMessage(), e);
+ } catch (AiravataSecurityException e) {
+ logger.error(e.getMessage(), e);
+ } catch (EntitlementPolicyAdminServiceEntitlementException e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+ }.start();
+ }
+}
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultXACMLPEP.java
----------------------------------------------------------------------
diff --git a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultXACMLPEP.java b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultXACMLPEP.java
index b60069c..71ced3a 100644
--- a/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultXACMLPEP.java
+++ b/airavata-api/airavata-api-server/src/main/java/org/apache/airavata/api/server/security/DefaultXACMLPEP.java
@@ -47,7 +47,8 @@ import java.rmi.RemoteException;
import java.util.Map;
/**
- * This enforces XACML based fine grained authorization on the API calls.
+ * This enforces XACML based fine grained authorization on the API calls, by authorizing the API calls
+ * through default PDP which is WSO2 Identity Server.
*/
public class DefaultXACMLPEP {
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/modules/commons/src/main/java/org/apache/airavata/common/utils/Constants.java
----------------------------------------------------------------------
diff --git a/modules/commons/src/main/java/org/apache/airavata/common/utils/Constants.java b/modules/commons/src/main/java/org/apache/airavata/common/utils/Constants.java
index 215a313..af8ca96 100644
--- a/modules/commons/src/main/java/org/apache/airavata/common/utils/Constants.java
+++ b/modules/commons/src/main/java/org/apache/airavata/common/utils/Constants.java
@@ -50,6 +50,8 @@ public final class Constants {
public static final String DENY = "Deny";
public static final String PERMIT = "Permit";
+ public static final String AUTHORIZATION_POLICY_NAME = "authorization.policy";
+
//Names of the attributes that could be passed in the AuthzToken's claims map.
public static final String USER_NAME = "userName";
public static final String EMAIL = "email";
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/modules/commons/src/main/java/org/apache/airavata/common/utils/ServerSettings.java
----------------------------------------------------------------------
diff --git a/modules/commons/src/main/java/org/apache/airavata/common/utils/ServerSettings.java b/modules/commons/src/main/java/org/apache/airavata/common/utils/ServerSettings.java
index b898d96..d87da70 100644
--- a/modules/commons/src/main/java/org/apache/airavata/common/utils/ServerSettings.java
+++ b/modules/commons/src/main/java/org/apache/airavata/common/utils/ServerSettings.java
@@ -269,7 +269,7 @@ public class ServerSettings extends ApplicationSettings {
return Boolean.valueOf(getSetting(Constants.IS_API_SECURED));
}
- public static String getRemoteOauthServerUrl() throws ApplicationSettingsException {
+ public static String getRemoteAuthzServerUrl() throws ApplicationSettingsException {
return getSetting(Constants.REMOTE_OAUTH_SERVER_URL);
}
@@ -281,6 +281,10 @@ public class ServerSettings extends ApplicationSettings {
return getSetting(Constants.ADMIN_PASSWORD);
}
+ public static String getAuthorizationPoliyName() throws ApplicationSettingsException{
+ return getSetting(Constants.AUTHORIZATION_POLICY_NAME);
+ }
+
public static String getZookeeperConnection() throws ApplicationSettingsException {
return getSetting(ZOOKEEPER_SERVER_CONNECTION, "localhost:2181");
}
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/modules/configuration/server/src/main/resources/airavata-default-xacml-policy.xml
----------------------------------------------------------------------
diff --git a/modules/configuration/server/src/main/resources/airavata-default-xacml-policy.xml b/modules/configuration/server/src/main/resources/airavata-default-xacml-policy.xml
index b0ca91e..a8fbf4c 100644
--- a/modules/configuration/server/src/main/resources/airavata-default-xacml-policy.xml
+++ b/modules/configuration/server/src/main/resources/airavata-default-xacml-policy.xml
@@ -1,4 +1,4 @@
-<Policy xmlns="urn:oasis:names:tc:xacml:3.0:core:schema:wd-17" PolicyId="airavata-policy"
+<Policy xmlns="urn:oasis:names:tc:xacml:3.0:core:schema:wd-17" PolicyId="airavata-default-xacml-policy"
RuleCombiningAlgId="urn:oasis:names:tc:xacml:3.0:rule-combining-algorithm:permit-overrides" Version="1.0">
<Target/>
<Rule Effect="Permit" RuleId="admin-permit">
http://git-wip-us.apache.org/repos/asf/airavata/blob/59f4acda/modules/configuration/server/src/main/resources/airavata-server.properties
----------------------------------------------------------------------
diff --git a/modules/configuration/server/src/main/resources/airavata-server.properties b/modules/configuration/server/src/main/resources/airavata-server.properties
index 0045935..58a42a3 100644
--- a/modules/configuration/server/src/main/resources/airavata-server.properties
+++ b/modules/configuration/server/src/main/resources/airavata-server.properties
@@ -237,5 +237,6 @@ keystore.password=airavata
trust.store=client_truststore.jks
trust.store.password=airavata
remote.oauth.authorization.server=https://localhost:9443/services/
+authorization.policy=airavata-default-xacml-policy
admin.user.name=admin
admin.password=admin
\ No newline at end of file