The device can only publish to their channels (channel d-{device-id}/temp)
The user can subscribe to any or all channels that the devices he owns publish. SO if he has two devices he can subscribe to d-1/temp and d-2/temp, but also to +/temp, this will be used for WebSocket connection (to see live data changes)
My first idea was to call the database in the authenticator and store devices id’s in ConnectionAttributeStore and validate topics in authorizer, but if I can do everything during authentication then I will be able to avoid creating authorizer.
Is the sample in the docs incomplete?
EDIT:
the sample in the docs is wrong.
It should be: if (input.getSubscription().getTopicFilter().startsWith("admin")) {
instead of if (input.getTopicFilter().startsWith("admin")) {
While trying to implement both SubscriptionAuthorizer and PublishAuthorizer I can’t get authorizeSubscribe to work. authorizePublish works fine, but the second isn’t. @FloLi any ideas why? I’m using Community Edition (if that changes something).
package com.test;
import com.hivemq.extension.sdk.api.annotations.NotNull;
import com.hivemq.extension.sdk.api.auth.PublishAuthorizer;
import com.hivemq.extension.sdk.api.auth.SubscriptionAuthorizer;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.PublishAuthorizerOutput;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerInput;
import com.hivemq.extension.sdk.api.auth.parameter.SubscriptionAuthorizerOutput;
import com.hivemq.extension.sdk.api.client.parameter.ConnectionAttributeStore;
import com.hivemq.extension.sdk.api.packets.disconnect.DisconnectReasonCode;
import com.hivemq.extension.sdk.api.packets.general.UserProperties;
import com.hivemq.extension.sdk.api.packets.publish.PublishPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
public class DatabaseAuthorizer implements SubscriptionAuthorizer, PublishAuthorizer {
private static final @NotNull
Logger log = LoggerFactory.getLogger(DatabaseAuthorizer.class);
@Override
public void authorizeSubscribe(@NotNull SubscriptionAuthorizerInput input, @NotNull SubscriptionAuthorizerOutput output) {
log.info("authorizeSubscribe");
final ConnectionAttributeStore attributeStore = input.getConnectionInformation().getConnectionAttributeStore();
Optional<String> deviceId = attributeStore.getAsString("deviceId");
if(!deviceId.isPresent()){
log.info("No deviceId in ConnectionAttributeStore");
} else
{
log.info("DeviceId in ConnectionAttributeStore = '{}'", deviceId.get());
}
//allow every Topic Filter starting with "admin"
if (input.getSubscription().getTopicFilter().startsWith("admin")) {
output.authorizeSuccessfully();
return;
}
//disallow a shared subscription
if (input.getSubscription().getTopicFilter().startsWith("$shared")) {
output.failAuthorization();
return;
}
//get the user properties from the SUBSCRIBE packet
final UserProperties userProperties = input.getUserProperties();
//disconnect all clients with a user property "notallowed" set.
if (userProperties.getFirst("notallowed").isPresent()) {
//Use a custom reason code and reason string for the server sent DISCONNECT
output.disconnectClient(DisconnectReasonCode.ADMINISTRATIVE_ACTION, "User property not allowed");
return;
}
//otherwise let the other extension or default permissions decide
output.nextExtensionOrDefault();
}
@Override
public void authorizePublish(@NotNull PublishAuthorizerInput input, @NotNull PublishAuthorizerOutput output) {
log.info("authorizePublish");
//get the PUBLISH packet contents
final PublishPacket publishPacket = input.getPublishPacket();
//allow every Topic Filter starting with "admin"
if (publishPacket.getTopic().startsWith("admin")) {
output.authorizeSuccessfully();
return;
}
//disallow publishes to forbidden topics
if (publishPacket.getTopic().startsWith("forbidden")) {
output.failAuthorization();
return;
}
//get the user properties from the PUBLISH packet
final UserProperties userProperties = publishPacket.getUserProperties();
//disconnect all clients with a user property "notallowed" set.
if (userProperties.getFirst("notallowed").isPresent()) {
//Use a custom reason code and reason string for the server sent DISCONNECT
output.disconnectClient(DisconnectReasonCode.ADMINISTRATIVE_ACTION, "User property not allowed");
return;
}
//otherwise let the other extension or default permissions decide
output.nextExtensionOrDefault();
}
}
When I try to publish I see in log authorizePublish but when I try to subscribe I don’t see authorizeSubscribe.
I register my class like this: securityRegistry.setAuthorizerProvider(authorizerProviderInput -> new DatabaseAuthorizer());
Thanks for the advice for the documentation issue.
Yes, thats correct.
Depends on your usecase.
Simple authentication (username,password) will be called once and for updates you need to reconnect, the CONNECT performance will decrease but on the other hand the PUBLISH / SUBSCRIBE performance would increase if everything will be done in authentication.
Let’s say if your clients do not need live updates for the permissions and they publish/subscribe very often compared to connect/disconnect than authentication would be a good way.
the code looks totally fine. This should work.
Do you have any other extension activated that has a SubscriptionAuthorizer?
This could cause that the other one will never be called.
Thank you for reply.
I’ve created a new extension and copy/paste code from my old one. Now everything works fine
For now I’m only adding log messages in authorizePublish and authorizeSubscribe
Now I need to do the logic.
I have one more question, this time more Java-related (as I wrote before I’m not a Java programmer)
When doing authentication I’m checking for login/pass pair in the database. Then I’m querying for all the device_id’s that the user owns.
I’m using them to build topic permissions (similar to file rbac extension).
I’d like to store id’s into ConnectionAttributeStore to be able to access the same list in my authorization class.
Here is my code that I’ve added to onConnect
final ConnectionAttributeStore attributeStore = input.getConnectionInformation().getConnectionAttributeStore();
var list = new ArrayList<String>();
list.add("d-1");
list.add("d-2");
list.add("d3");
attributeStore.put("devices",??);
For now, I’m serializing/deserializing with above code, but I’m sure there is a better way to store and read a list of strings in ConnectionAttributeStore
var list = new ArrayList<String>();
list.add("d-1");
list.add("d-2");
list.add("d3");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(list);
byte[] bytes = bos.toByteArray();
ByteBuffer buffer = ByteBuffer.wrap(bytes);
ByteArrayInputStream bis = new ByteArrayInputStream(buffer.array());
ObjectInput oi = new ObjectInputStream(bis);
ArrayList<String> devices = (ArrayList<String>) oi.readObject();
currently in onConnect I’m using below code to set permissions per user:
final ArrayList<TopicPermission> topicPermissions = new ArrayList<>();
for (String deviceId: user.get().getDevices()) {
final TopicPermission publishPermission = Builders.topicPermission()
.topicFilter(deviceId+"/settings")
.activity(TopicPermission.MqttActivity.PUBLISH)
.type(TopicPermission.PermissionType.ALLOW)
.build();
topicPermissions.add(publishPermission);
final TopicPermission subscribePermission = Builders.topicPermission()
.topicFilter(deviceId+"/values")
.activity(TopicPermission.MqttActivity.SUBSCRIBE)
.type(TopicPermission.PermissionType.ALLOW)
.build();
topicPermissions.add(subscribePermission);
}
output.getDefaultPermissions().addAll(topicPermissions);
output.getDefaultPermissions().setDefaultBehaviour(DefaultAuthorizationBehaviour.DENY);
output.authenticateSuccessfully();
this works great in basic scenario, but I want to add support for subscribing to +/values topic.
Idea is that when user1 subscribes to that topic he will only get messages from devices 1,2 and 3 (because he owns them).
In the linked response you showed that this can be done in authorizeSubscribe, but can I set topic permissions in onConnect? This way user could subscribe to get values from all devices and the broker would handle sending values from device to correct user.