diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java index 61b52c875d..3da445eb85 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapIntegrationTestHelper.java @@ -32,6 +32,7 @@ import java.security.spec.ECPublicKeySpec; import java.security.spec.KeySpec; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import org.eclipse.leshan.LwM2mId; @@ -42,8 +43,10 @@ import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.resource.SimpleInstanceEnabler; import org.eclipse.leshan.core.request.Identity; import org.eclipse.leshan.server.bootstrap.BootstrapConfig; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity; import org.eclipse.leshan.server.bootstrap.BootstrapStore; @@ -97,36 +100,7 @@ public BootstrapIntegrationTestHelper() { public void createBootstrapServer(BootstrapSecurityStore securityStore, BootstrapStore bootstrapStore) { if (bootstrapStore == null) { - bootstrapStore = new BootstrapStore() { - @Override - public BootstrapConfig getBootstrap(String endpoint, Identity deviceIdentity) { - BootstrapConfig bsConfig = new BootstrapConfig(); - - // security for BS server - ServerSecurity bsSecurity = new ServerSecurity(); - bsSecurity.serverId = 1111; - bsSecurity.bootstrapServer = true; - bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" - + bootstrapServer.getUnsecuredAddress().getPort(); - bsSecurity.securityMode = SecurityMode.NO_SEC; - bsConfig.security.put(0, bsSecurity); - - // security for DM server - ServerSecurity dmSecurity = new ServerSecurity(); - dmSecurity.uri = "coap://" + server.getUnsecuredAddress().getHostString() + ":" - + server.getUnsecuredAddress().getPort(); - dmSecurity.serverId = 2222; - dmSecurity.securityMode = SecurityMode.NO_SEC; - bsConfig.security.put(1, dmSecurity); - - // DM server - ServerConfig dmConfig = new ServerConfig(); - dmConfig.shortId = 2222; - bsConfig.servers.put(0, dmConfig); - - return bsConfig; - } - }; + bootstrapStore = unsecuredBootstrapStore(); } if (securityStore == null) { @@ -186,7 +160,7 @@ private void createClient(Security security) { initializer.setInstancesForObject(LwM2mId.SECURITY, security); initializer.setInstancesForObject(LwM2mId.DEVICE, new Device("Eclipse Leshan", IntegrationTestHelper.MODEL_NUMBER, "12345", "U")); - initializer.setClassForObject(LwM2mId.ACCESS_CONTROL, DummyInstanceEnabler.class); + initializer.setClassForObject(LwM2mId.ACCESS_CONTROL, SimpleInstanceEnabler.class); initializer.setClassForObject(LwM2mId.SERVER, DummyInstanceEnabler.class); List objects = initializer.createAll(); @@ -253,6 +227,92 @@ public List getAllByEndpoint(String endpoint) { }; } + public BootstrapStore unsecuredBootstrapStore() { + return new BootstrapStore() { + + @Override + public BootstrapConfig getBootstrap(String endpoint, Identity deviceIdentity) { + + BootstrapConfig bsConfig = new BootstrapConfig(); + + // security for BS server + ServerSecurity bsSecurity = new ServerSecurity(); + bsSecurity.serverId = 1111; + bsSecurity.bootstrapServer = true; + bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" + + bootstrapServer.getUnsecuredAddress().getPort(); + bsSecurity.securityMode = SecurityMode.NO_SEC; + bsConfig.security.put(0, bsSecurity); + + // security for DM server + ServerSecurity dmSecurity = new ServerSecurity(); + dmSecurity.uri = "coap://" + server.getUnsecuredAddress().getHostString() + ":" + + server.getUnsecuredAddress().getPort(); + dmSecurity.serverId = 2222; + dmSecurity.securityMode = SecurityMode.NO_SEC; + bsConfig.security.put(1, dmSecurity); + + // DM server + ServerConfig dmConfig = new ServerConfig(); + dmConfig.shortId = 2222; + bsConfig.servers.put(0, dmConfig); + + return bsConfig; + } + }; + } + + public BootstrapStore unsecuredWithAclBootstrapStore() { + return new BootstrapStore() { + + @Override + public BootstrapConfig getBootstrap(String endpoint, Identity deviceIdentity) { + + BootstrapConfig bsConfig = new BootstrapConfig(); + + // security for BS server + ServerSecurity bsSecurity = new ServerSecurity(); + bsSecurity.serverId = 1111; + bsSecurity.bootstrapServer = true; + bsSecurity.uri = "coap://" + bootstrapServer.getUnsecuredAddress().getHostString() + ":" + + bootstrapServer.getUnsecuredAddress().getPort(); + bsSecurity.securityMode = SecurityMode.NO_SEC; + bsConfig.security.put(0, bsSecurity); + + // security for DM server + ServerSecurity dmSecurity = new ServerSecurity(); + dmSecurity.uri = "coap://" + server.getUnsecuredAddress().getHostString() + ":" + + server.getUnsecuredAddress().getPort(); + dmSecurity.serverId = 2222; + dmSecurity.securityMode = SecurityMode.NO_SEC; + bsConfig.security.put(1, dmSecurity); + + // DM server + ServerConfig dmConfig = new ServerConfig(); + dmConfig.shortId = 2222; + bsConfig.servers.put(0, dmConfig); + + // ACL + ACLConfig aclConfig = new ACLConfig(); + aclConfig.objectId = 3; + aclConfig.objectInstanceId = 0; + HashMap acl = new HashMap(); + acl.put(3333, 1l); // server with short id 3333 has just read(1) right on device object (3/0) + aclConfig.acls = acl; + aclConfig.AccessControlOwner = 2222; + bsConfig.acls.put(0, aclConfig); + + aclConfig = new ACLConfig(); + aclConfig.objectId = 4; + aclConfig.objectInstanceId = 0; + aclConfig.AccessControlOwner = 2222; + bsConfig.acls.put(1, aclConfig); + + return bsConfig; + } + }; + } + public BootstrapStore pskBootstrapStore() { return new BootstrapStore() { diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java index 1fc439858b..7d7e8af064 100644 --- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java +++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java @@ -16,8 +16,15 @@ package org.eclipse.leshan.integration.tests; import static org.eclipse.leshan.integration.tests.SecureIntegrationTestHelper.*; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.*; import org.eclipse.leshan.SecurityMode; +import org.eclipse.leshan.client.request.ServerIdentity; +import org.eclipse.leshan.core.node.LwM2mObject; +import org.eclipse.leshan.core.node.LwM2mObjectInstance; +import org.eclipse.leshan.core.request.ReadRequest; +import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; import org.junit.After; @@ -63,6 +70,43 @@ public void bootstrap() { helper.assertClientRegisterered(); } + @Test + public void bootstrapWithAcl() { + // Create DM Server without security & start it + helper.createServer(); + helper.server.start(); + + // Create and start bootstrap server + helper.createBootstrapServer(null, helper.unsecuredWithAclBootstrapStore()); + helper.bootstrapServer.start(); + + // Create Client and check it is not already registered + helper.createClient(); + helper.assertClientNotRegisterered(); + + // Start it and wait for registration + helper.client.start(); + helper.waitForRegistrationAtServerSide(1); + + // check the client is registered + helper.assertClientRegisterered(); + + // ensure ACL is correctly set + ReadResponse response = helper.client.getObjectEnablers().get(2).read(ServerIdentity.SYSTEM, + new ReadRequest(2)); + LwM2mObject acl = (LwM2mObject) response.getContent(); + assertThat(acl.getInstances().keySet(), hasItems(0, 1)); + LwM2mObjectInstance instance = acl.getInstance(0); + assertEquals(3l, instance.getResource(0).getValue()); + assertEquals(0l, instance.getResource(1).getValue()); + assertEquals(1l, instance.getResource(2).getValues().get(3333)); + assertEquals(2222l, instance.getResource(3).getValue()); + instance = acl.getInstance(1); + assertEquals(4l, instance.getResource(0).getValue()); + assertEquals(0l, instance.getResource(1).getValue()); + assertEquals(2222l, instance.getResource(3).getValue()); + } + @Test public void bootstrapSecureWithPSK() { // Create DM Server without security & start it diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java index 86d53a1435..3ee9a80c1a 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapConfig.java @@ -33,6 +33,8 @@ public class BootstrapConfig implements Serializable { public Map security = new HashMap<>(); + public Map acls = new HashMap<>(); + /** server configuration (object 1) */ static public class ServerConfig implements Serializable { public int shortId; @@ -45,10 +47,9 @@ static public class ServerConfig implements Serializable { @Override public String toString() { - return String - .format("ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s]", - shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, - binding); + return String.format( + "ServerConfig [shortId=%s, lifetime=%s, defaultMinPeriod=%s, defaultMaxPeriod=%s, disableTimeout=%s, notifIfDisabled=%s, binding=%s]", + shortId, lifetime, defaultMinPeriod, defaultMaxPeriod, disableTimeout, notifIfDisabled, binding); } } @@ -71,18 +72,30 @@ static public class ServerSecurity implements Serializable { @Override public String toString() { // Note : secretKey and smsBindingKeySecret are explicitly excluded from the display for security purposes - return String - .format("ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeySecret=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s]", - uri, bootstrapServer, securityMode, Arrays.toString(publicKeyOrId), - Arrays.toString(serverPublicKey), smsSecurityMode, - Arrays.toString(smsBindingKeyParam), serverSmsNumber, - serverId, clientOldOffTime, bootstrapServerAccountTimeout); + return String.format( + "ServerSecurity [uri=%s, bootstrapServer=%s, securityMode=%s, publicKeyOrId=%s, serverPublicKey=%s, smsSecurityMode=%s, smsBindingKeySecret=%s, serverSmsNumber=%s, serverId=%s, clientOldOffTime=%s, bootstrapServerAccountTimeout=%s]", + uri, bootstrapServer, securityMode, Arrays.toString(publicKeyOrId), + Arrays.toString(serverPublicKey), smsSecurityMode, Arrays.toString(smsBindingKeyParam), + serverSmsNumber, serverId, clientOldOffTime, bootstrapServerAccountTimeout); + } + } + + /** server configuration (object 1) */ + static public class ACLConfig implements Serializable { + public int objectId; + public int objectInstanceId; + public Map acls; + public Integer AccessControlOwner; + + @Override + public String toString() { + return String.format("ACLConfig [objectId=%s, objectInstanceId=%s, ACLs=%s, AccessControlOwner=%s]", + objectId, objectInstanceId, acls, AccessControlOwner); } } @Override public String toString() { - return String.format("BootstrapConfig [servers=%s, security=%s]", servers, security); + return String.format("BootstrapConfig [servers=%s, security=%s, acls=%s]", servers, security, acls); } - } diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java index cb30927df9..094bba5101 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapFailureCause.java @@ -31,6 +31,10 @@ public enum BootstrapFailureCause { * The "/" object could not be deleted on the device */ DELETE_FAILED, + /** + * Object 2 (ACL) could not be written on the device + */ + WRITE_ACL_FAILED, /** * Object 1 (Server) could not be written on the device */ diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java index 0f95fac55e..4076a4f05f 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/bootstrap/BootstrapHandler.java @@ -23,6 +23,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; @@ -41,6 +42,7 @@ import org.eclipse.leshan.core.response.ErrorCallback; import org.eclipse.leshan.core.response.LwM2mResponse; import org.eclipse.leshan.core.response.ResponseCallback; +import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ACLConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerConfig; import org.eclipse.leshan.server.bootstrap.BootstrapConfig.ServerSecurity; import org.slf4j.Logger; @@ -191,6 +193,40 @@ public void onError(Exception e) { sessionManager.failed(session, WRITE_SERVER_FAILED, writeServerRequest); } }); + } else { + // we are done, send the ACL + List aclsToSend = new ArrayList<>(cfg.acls.keySet()); + sendAcls(session, cfg, aclsToSend); + } + } + + private void sendAcls(final BootstrapSession session, final BootstrapConfig cfg, final List toSend) { + if (!toSend.isEmpty()) { + // get next config + Integer key = toSend.remove(0); + ACLConfig aclConfig = cfg.acls.get(key); + + // extract write request parameters + LwM2mPath path = new LwM2mPath(2, key); + final LwM2mNode aclInstance = convertToAclInstance(key, aclConfig); + + final BootstrapWriteRequest writeACLRequest = new BootstrapWriteRequest(path, aclInstance, + session.getContentFormat()); + send(session, writeACLRequest, new ResponseCallback() { + @Override + public void onResponse(BootstrapWriteResponse response) { + LOG.trace("Bootstrap write {} return code {}", session.getEndpoint(), response.getCode()); + // recursive call until toSend is empty + sendAcls(session, cfg, toSend); + } + }, new ErrorCallback() { + @Override + public void onError(Exception e) { + LOG.warn(String.format("Error during bootstrap write of acl instance %s on %s", aclInstance, + session.getEndpoint()), e); + sessionManager.failed(session, WRITE_ACL_FAILED, writeACLRequest); + } + }); } else { final BootstrapFinishRequest finishBootstrapRequest = new BootstrapFinishRequest(); send(session, finishBootstrapRequest, new ResponseCallback() { @@ -268,4 +304,17 @@ private LwM2mObjectInstance convertToServerInstance(int instanceId, ServerConfig return new LwM2mObjectInstance(instanceId, resources); } + + private LwM2mObjectInstance convertToAclInstance(int instanceId, ACLConfig aclConfig) { + Collection resources = new ArrayList<>(); + + resources.add(LwM2mSingleResource.newIntegerResource(0, aclConfig.objectId)); + resources.add(LwM2mSingleResource.newIntegerResource(1, aclConfig.objectInstanceId)); + if (aclConfig.acls != null) + resources.add(LwM2mMultipleResource.newIntegerResource(2, aclConfig.acls)); + if (aclConfig.AccessControlOwner != null) + resources.add(LwM2mSingleResource.newIntegerResource(3, aclConfig.AccessControlOwner)); + + return new LwM2mObjectInstance(instanceId, resources); + } }