Microservices Best Practices: Tenant vs User Scope API access in Java SDK

Overview

When using the Microservice Java SDK for the first time you might struggle with an error:

java.lang.IllegalStateException: Not within any context!

This is because each request against cumulocity must be authenticated and you have to decide if you want to use the so called service user or an individual authenticated user.

Personal remark: I’m aware of that the official documentation currently contains examples I’m not referring in my article. The main reason is that I did comprehensive testing and found that the examples in the documentation are not working as expected. I created an incident to either get this fixed in the SDK and/or documentation. All my code examples and ways described in this article are tested and are working as expected

Service user vs. individual authenticated user.

Service user

The Service user is a technical user that is generated by the platform for each tenant the microservice is subscribed to. Meaning, if you subscribe your microservice to X tenants you also get X service user credentials to authenticate to each of them.

Service users only use global roles which are specified as part of the manifest of the microservice. In the property requiredRoles the developer of the microservice decides which global permissions are required to run the microservice properly.
Here is an example which allows the microservice to read ALL objects in the inventory and it external IDs:

"requiredRoles": [
    "ROLE_INVENTORY_READ",
    "ROLE_IDENTITY_READ",
  ],

Individual authenticated user

The individual user account is a user account that has been created by the tenant administrator. Normally it is assigned to a specific person with a name, email and individual password defined by this person.

These kind of users might have global roles assigned to access full parts of an API or use inventory roles where it can be on group level defined if this user should have access to specific devices or not.
Normally these users are the main user of the platform and are authenticated individually by using their credentials. This authentication context can be also used within a microservice if desired. A common use case is to only fetch the devices the user has access to and not all available devices the service user might retrieve.

As a microservice developer you have to decide if you want to use the service user or individual authenticated user context when accessing the Cumulocity API.

Using the service user

Let’s start with the most common use case using the service user to access the Cumulocity API. In the java SDK we have dedicated services to do that.
Mainly you can use the MicroserviceSubscriptionsService to run some logic in a specific context. This can be used for any kind of threads including scheduler threads as you don’t need any user input/data.

In the code snippet below we use this service to switch to the tenant context of each tenant. So the logic is executed for each tenant the microservice is subscribed to and will return all available managed objects across all tenants.

@Autowired
MicroserviceSubscriptionsService subscriptionsService;

@Autowired
InventoryApi tenantInventoryApi;

public List<ManagedObjectRepresentation> getAllDevicesTenant2() {
	List<ManagedObjectRepresentation> morList = new ArrayList<>();
	subscriptionsService.runForEachTenant(() -> {
		tenantInventoryApi.getManagedObjects().get().allPages().forEach(mor -> {
			morList.add(mor);
		});
	});
	return morList;
}

You can only iterate over all subscribed tenants within one microservice instance when using isolation level MULTI_TENANT. When using PER_TENANT the microservice instance will only access the one tenant it is deployed to, which will also lead to have multiple microservice instances when subscribed to multiple tenants.

Within that context we can now access all other available API the service user has access to (defined in the manifest of the microservice).

There are two cases in the Java SDK where you already are in a given context and you don’t have to call the MicroserviceSubscriptionsService:

  1. When using the annotation @EventListener for MicroserviceSubscriptionAddedEvent. Here you are in the context of the tenant which the microservice is subscribed to. On startup of the microservice the method will be executed for each of the active subscribed tenants.

Example:

@EventListener
public void initialize(MicroserviceSubscriptionAddedEvent event) {
   String tenant = event.getCredentials().getTenant();
   log.info("Tenant {} - Microservice subscribed", tenant);
   tenantInventoryApi.getManagedObjects().get().allPages();
}
  1. When you use a RestController and directly call any API which per default uses the service user of the authenticated context.

Caution Very often it is wrongly assumed that you are in the context of the authenticated user but this isn’t the case. Actually the default service user is used, even an individual user authenticated against the REST endpoint of the microservice.

Example:

    @GetMapping(path = "/devicesTenant", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<ManagedObjectRepresentation>> getAllDevicesTenant() {
        List<ManagedObjectRepresentation> response = deviceService.getAllDevicesTenant();
        return new ResponseEntity<>(response, HttpStatus.OK);
    }

With the service method:

public List<ManagedObjectRepresentation> getAllDevicesTenant() {
	List<ManagedObjectRepresentation> morList = new ArrayList<>();
	tenantInventoryApi.getManagedObjects().get().allPages().forEach(mor -> {
		morList.add(mor);
	});
	return morList;
}

The results of all explained ways are always the same.

Using the individual authenticated user

If you want to use the authenticated user you obviously need the user data from somewhere. So the main use case you can use it are Rest Controllers and endpoints which are exposed by the microservice.

Let’s start with an example of a RestController endpoint which is simply calling the device service:

@GetMapping(path = "/devicesUser", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ManagedObjectRepresentation>> getAllDevicesUser() {
	List<ManagedObjectRepresentation> response = deviceService.getAllDevicesUser();
	return new ResponseEntity<>(response, HttpStatus.OK);
}

In the deviceService we can specify now that we want to use the authenticated user. This is done by adding a @Qualifier("userInventoryApi") to the InventoryApi defining not to use the default service user but the user context.

With that we can just call the API as usual but the output will be totally different to using the service user of course.

@Autowired
@Qualifier("userInventoryApi")
InventoryApi userInventoryApi;

public List<ManagedObjectRepresentation> getAllDevicesUser() {
	List<ManagedObjectRepresentation> morList = new ArrayList<>();
	userInventoryApi.getManagedObjects().get().allPages().forEach(mor -> {
		morList.add(mor);
	});
	return morList;
}

The most important part is using the @Qualifier which is unfortunately not very well documented. For that reasons I listed all available @Qualifier below:

@Override
@UserScope
@Bean(name = "userInventoryApi")
public InventoryApi getInventoryApi() throws SDKException {
	return delegate.getInventoryApi();
}

@Override
@UserScope
@Bean(name = "userIdentityApi")
public IdentityApi getIdentityApi() throws SDKException {
	return delegate.getIdentityApi();
}

@Override
@UserScope
@Bean(name = "userMeasurementApi")
public MeasurementApi getMeasurementApi() throws SDKException {
	return delegate.getMeasurementApi();
}

@Override
@UserScope
@Bean(name = "userDeviceControlApi")
public DeviceControlApi getDeviceControlApi() throws SDKException {
	return delegate.getDeviceControlApi();
}

@Override
@UserScope
@Bean(name = "userAlarmApi")
public AlarmApi getAlarmApi() throws SDKException {
	return delegate.getAlarmApi();
}

@Override
@UserScope
@Bean(name = "userEventApi")
public EventApi getEventApi() throws SDKException {
	return delegate.getEventApi();
}

@Override
@UserScope
@Bean(name = "userAuditRecordApi")
public AuditRecordApi getAuditRecordApi() throws SDKException {
	return delegate.getAuditRecordApi();
}

@Override
@UserScope
@Bean(name = "userDeviceCredentialsApi")
public DeviceCredentialsApi getDeviceCredentialsApi() throws SDKException {
	return delegate.getDeviceCredentialsApi();
}

@Override
@UserScope
@Bean(name = "userBinariesApi")
public BinariesApi getBinariesApi() throws SDKException {
	return delegate.getBinariesApi();
}

@Override
@UserScope
@Bean(name = "userUserApi")
public UserApi getUserApi() throws SDKException {
	return delegate.getUserApi();
}

@Override
@UserScope
@Bean(name = "userTenantOptionApi")
public TenantOptionApi getTenantOptionApi() throws SDKException {
	return delegate.getTenantOptionApi();
}

@Override
@UserScope
@Bean(name = "userSystemOptionApi")
public SystemOptionApi getSystemOptionApi() throws SDKException {
	return delegate.getSystemOptionApi();
}

@Override
@UserScope
@Bean(name = "userTokenApi")
public TokenApi getTokenApi() throws SDKException {
	return delegate.getTokenApi();
}

@Override
@UserScope
@Bean(name = "userNotificationSubscriptionApi")
public NotificationSubscriptionApi getNotificationSubscriptionApi() throws SDKException {
	return delegate.getNotificationSubscriptionApi();
}

You can also discover them yourself be checking the following sources: cumulocity-clients-java/microservice/api/src/main/java/com/cumulocity/microservice/api/CumulocityClientFeature.java at develop · SoftwareAG/cumulocity-clients-java · GitHub

Another way to use the authenticated user context is to initiate a userPlatform and calling the API via the platformAPI.
Here is an example how this can be done:

@Autowired(required = true)
@Qualifier("userPlatform")
private Platform platformApi;

public List<ManagedObjectRepresentation> getAllDevicesUser2() {
	List<ManagedObjectRepresentation> morList = new ArrayList<>();
	platformApi.getInventoryApi().getManagedObjects().get().allPages().forEach(mor -> {
		morList.add(mor);
	});
	return morList;
}

Both methods will pass the authenticated user credentials to the API and will reflect in access to the API which is assigned to the user.

Summary

In this article I described multiple ways how you can leverage the Microservice Java SDK to either use the service user or the authenticated individual user. Which one do you use heavily depends on your user case. In scheduler and user-independent use case you might should use the service user. If you have a REST Controller and you want to reflect the authorization of an individual user then you may use the authenticated user context to call the API. Common use cases are permission checks or creating objects in the name of individual users and not a technical user for audit reasons.

All my used examples are published in this GitHub Repo:

Feel free to clone it and play around with all ways I described.
If you have any feedback or additions just leave a comment and I would be happy to discuss it with you!

3 Likes