[C8Y Java SDK] Error while creating externalID

Product/components used and version/fix level:

18

Detailed explanation of the problem:

Hi guys,

I am using JAVA SDK, when I execute the below code to create a new externalID for a device, I receive error 400 from SDK:

               ...build device object here
               newDevice = inventoryApi.create(newDevice );

                // add externalID to newDevice 
                ExternalIDRepresentation externalIDRepresentation = new ExternalIDRepresentation();
                externalIDRepresentation.setExternalId("unique_externalID");
                externalIDRepresentation.setType("c8y_Serial");
                externalIDRepresentation.setManagedObject(newDevice );
                identityApi.create(externalIDRepresentation);

I already defined the required roles: “ROLE_IDENTITY_READ” and “ROLE_IDENTITY_ADMIN”.

Error messages / full error message screenshot / log file:

Http status code: 400
Something went wrong. Failed to parse error message.
tenant: my_tenantID user: my_service_name

Question related to a free trial, or to a production (customer) instance?

production

Hey there! I have similar code which runs perfectly. Not too sure if anything is wrong with the code itself. Try if you can create the external id for the device via Postman, if at least it works there?



Arsalan Mahmood Siddiqi

1 Like

Hi Arsalan,

Thanks for your response.
Yes, I can create externalID via postman using this endpoint: /identity/globalIds/{deviceID}/externalIds (I believe Java SDK uses this API as well). The difference is that I use another account when using Postman (the login account, while I use bootstrap user inside microservice).

Can you share your code and auth configuration if possible? I’d very much appreciate that.

Thanks

Can you confirm that you do the call in a tenant scope and not using the service bootstrap user?

Hi Harald,

Can you ask more clearly? I am running the microservice locally (MULTI_TENANT mode), in my code snippet, you can see I can create ManagedObject first (so I don’t think there’s a problem with authen), the error occurs when I call “identityApi.create()”. Here is my config:


C8Y.bootstrap.register=true
C8Y.baseURL=mytenant/
C8Y.microservice.isolation=MULTI_TENANT
C8Y.bootstrap.tenant=tenantID

# local
application.name=app-name
application.key=app-key
C8Y.bootstrap.user=servicebootstrap_user
C8Y.bootstrap.password=bootstrap_password

For the managed object you create, can you check who is it owner? is it the service user or the service bootstrap user?

Here you can find more information about context support:
https://cumulocity.com/docs/microservice-sdk/java/#context-support

In general depending on how you execute an API call in a microservice, it is either using the user who called the microservice (if your microservice is called via REST), the bootstrap service user or the service user. In your case, you want to use the service user.

A way of doing that is to use the MicroserviceSubscriptionsService callForTenant or runForTenant methods:
http://resources.cumulocity.com/documentation/microservicesdk/current/com/cumulocity/microservice/subscription/service/MicroserviceSubscriptionsService.html

This is an example I could quickly find on github:

Thanks Harald,

I use the service bootstrap user to call the microsevice via REST. The owner of the managedObject is the service bootstrap user. I do know about the MicroserviceSubscriptionsService (I am using it in another microservice). But in this case, I can’t get the logic of the SDK, the error response is not clear as well.

The Bootstrap user cannot be used to make any other API Call then retrieving the service users which is actually implemented in the MicroserviceSubscriptionsService

What is the case you can’t use it or the SDK? Maybe it helps to post the full code block.

Do you understand my question? I am using the SDK, and in the same thread, I can create the managedObject but can’t create the externalID. If it is related to the user, it should fail when I create the managedObject.

You are writing that in “this case” you don’t get the logic of the SDK.
What is the case? Can you demonstrate how you authenticate using the SDK? How do you initialize the platform and inventory, identifyApi beans?

Harald and myself advise is pretty clear: Use the MicroserviceSubscriptionService and NOT any Bootstrap User. Even when you can create managed objects (for any reasons), it is not supported to do so. You should use the service user instead for creating all kind of objects in Cumulocity.

That is possibly the reason you are getting 400 status codes back. To help you we need just more information like the topics I mentioned above.

Hi Stefan,

Thanks for your suggestion. I switched the microservice to PER_TENANT mode and I still see the same unclear error.
Here is my configuration:

// cumulocity.json
C8Y.baseURL=https://mydomain.apj.cumulocity.com/
C8Y.microservice.isolation=PER_TENANT
C8Y.bootstrap.tenant=my_tenant_id
C8Y.bootstrap.user=servicebootstrap_my-app
C8Y.bootstrap.password=my-bootstrap-password

C8Y.tenant=my_tenant_id
C8Y.user=my-service-user (the user I am using to login to C8y)
C8Y.password=my-service-password

application.name=my-app-name
application.key=my-app-key

Step to run locally:

  1. Create an application with the required roles
  2. Get the bootstrap user
  3. Start the microservice
  4. Use Postman to call /decode endpoint (exposed by the microservice). I am using the servicebootstrap in Postman here.
  5. All logic runs well (create managed object, create group, sub group, set childAsset…) except the externalID

Here is error message:

com.cumulocity.sdk.client.SDKException: Http status code: 400
Something went wrong. Failed to parse error message.
 tenant: my_tenant_ID user: my_user

Note: Now I can see the owner of the created managed object is service use (which is configured as C8Y.user variable), not the servicebootstrap user.

Next: I will try to use MULTI_TENANT with MicroserviceSubscriptionService.
Can you help me to explain what is happening here? Why does that issue only happen with externalID?

Thanks

Hi,

can’t tell why you’re seeing this 400 response, I think we would need code snippets here. Which SDK version are you using?

In your forth step I wouldn’t use bootstrap credentials. These credentials are only meant to retrieve service-users for every subscribed tenant (via /application/currentApplication/subscriptions endpoint), they shouldn’t be used for other API calls and also not to authenticate a REST request against your service. See the three types of Users in the Microservice docs here.

Below is an example for a multi-tenant microservice that exposes an API endpoint. Once endpoint is hit, it creates a new Managed Object and an External ID in every tenant it is subscribed to.

application.properties:

c8y.version=@c8y.version@
microservice.version=@project.version@

application.name=techforum
application.key=techforum
C8Y.bootstrap.register=true
C8Y.bootstrap.tenant=t1234
C8Y.baseURL=https://t1234.eu-latest.cumulocity.com
C8Y.bootstrap.user=servicebootstrap_techforum
C8Y.bootstrap.password=super-secret-pass
C8Y.microservice.isolation=MULTI_TENANT

logging.level.org.springframework=info
logging.level.root=info

RestController:

@RestController
public class ExampleController {
    private ExampleService exampleService;

    public ExampleController(ExampleService exampleService) { this.exampleService = exampleService; }

    @PostMapping(path = "/createMo", produces = MediaType.TEXT_PLAIN_VALUE)
    @PreAuthorize("hasRole('ROLE_INVENTORY_ADMIN') and hasRole('ROLE_IDENTITY_ADMIN')")
    public ResponseEntity<String> createMo() {
        exampleService.createDevicesWithExternalId();
        return ResponseEntity.status(HttpStatus.CREATED).body("Created.");
    }
}

Service:

@Service
public class ExampleService {
    @Autowired
    MicroserviceSubscriptionsService subscriptionsService;
    @Autowired
    InventoryApi inventoryApi;
    @Autowired
    IdentityApi identityApi;

    public void createDevicesWithExternalId() {
        // create MO and external ID in each subscribed tenant
        // alternatively, use subscriptionsService.runForEachTenant(Runnable r)
        subscriptionsService.getAll().forEach(credentials -> {
            subscriptionsService.runForTenant(credentials.getTenant(), () -> {
                ManagedObjectRepresentation mo = new ManagedObjectRepresentation();
                mo.setType("techforum");
                ManagedObjectRepresentation mor = inventoryApi.create(mo);

                ExternalIDRepresentation externalIDRepresentation = new ExternalIDRepresentation();
                externalIDRepresentation.setExternalId(UUID.randomUUID().toString());
                externalIDRepresentation.setType("c8y_Serial");
                externalIDRepresentation.setManagedObject(mor);
                identityApi.create(externalIDRepresentation);
                System.out.println(
                        String.format(
                                "Created Managed Object: [id=%s, externalId=%s, owner=%s] in Tenant '%s' with User '%s'",
                                mor.getId().getValue(), externalIDRepresentation.getExternalId(), mor.getOwner(),
                                credentials.getTenant(), credentials.getUsername()));
            });
        });
        // or to create in specific tenant:
        // subscriptionsService.runForTenant("t12345", Runnable r);
    }
}

Now you should be able to start the service and do requests with any User of this tenant (not the bootstrap-user, not the service-user), for example:

curl -X POST http://localhost:8080/createMo -u 't1234/john.doe@cumulocity.com:{password of john.doe}' -v

In case john.doe has the required permissions (see preauthorize annotation), a Managed Object with an External ID is created. If john.doe is lacking the permissions, it will respond 403 Forbidden.

Can you check what’s your response when doing above?

Some notes that might help:

  • The request was authorized against your provided User in Controller, but the Object got created by the Service via the Service-User. Thus, owner will be your service user.
  • To see the permissions of a specific User, you can use the /user/currentUser , the effectiveRoles fragment lists the permissions. Can be used for regular users but also for bootstrap- and service-users.
  • With setting the log levels in appliation.properties to “debug” you’ll get a verbose output that logs the HTTP calls sent towards the platform.

Hi Korbinian,

I truly appreciate your detailed guidance. I figured out what caused the error, but I still don’t understand why. It’s not related to the authentication.
Here is the summary:

  1. SDK version: 1018.0.151
  2. The error only occurs in the APJ environment, the same code runs well with other env
  3. In APJ env:
  • The error only occurs if I set this property to the managedObject before creating externalID: com_cumulocity_model_Agent: {} (I also set it in the other env and it can run well)
  • Error code:
            ManagedObjectRepresentation mo = new ManagedObjectRepresentation();
            mo.setName("test");
            mo.setType("test");
            mo.setProperty("com_cumulocity_model_Agent", "{}"); // No error without this line
            mo = inventoryApi.create(mo);
            
            ExternalIDRepresentation externalIDRepresentation = new ExternalIDRepresentation();
            externalIDRepresentation.setExternalId(externalId);
            externalIDRepresentation.setType("c8y_Serial");
            externalIDRepresentation.setManagedObject(mo);

            identityApi.create(externalIDRepresentation);

This is not the 1st time I encountered an error in APJ. This is an error I’ve encountered before, and it seems like it was fixed at some point: https://tech.forums.softwareag.com/t/cumulocity-notificaion-2-listen-to-device-notifcation/284834/11?u=elcarim1802

1 Like

Ah, I think I see the problem now. The agent fragment is meant to be an object, your code tries to set it as String. Try replacing mo.setProperty("com_cumulocity_model_Agent", "{}") with mo.setProperty("com_cumulocity_model_Agent", new Object()) and it should work.

I also set it in the other env and it can run well

That one I wonder about. Because I can reproduce it on eu-latest.cumulocity.com via REST. E.g. this call fails with “400 Failed to parse JSON string”:

POST /inventory/managedObjects HTTP/1.1
Host: {host}
Accept: application/json
Authorization: Basic  {base64 tenant/username:password}
Content-Type: application/json

{"com_cumulocity_model_Agent":"{}"}

While this one works well (with com_cumulocity_model_Agent being an object, not a string):

POST /inventory/managedObjects HTTP/1.1
Host: {host}
Accept: application/json
Authorization: Basic  {base64 tenant/username:password}
Content-Type: application/json

{"com_cumulocity_model_Agent":{}}
1 Like

You are right, it works fine if I use an object instead of a String. :laughing:
I also use String for other properties but only the agent fragment caused that error, haha.
And I don’t know what is SDK doing with managedObject properties when creating externalID, while it only uses managedObject’s ID

     // SDK source
     public ExternalIDRepresentation create(ExternalIDRepresentation representation) throws SDKException {
        if (representation != null && representation.getManagedObject() != null && representation.getManagedObject().getId() != null) {
            Map<String, String> filter = new HashMap();
            filter.put("globalId", representation.getManagedObject().getId().getValue());
            String path = this.templateUrlParser.replacePlaceholdersWithParams(this.getIdentityRepresentation().getExternalIdsOfGlobalId(), filter);
            return (ExternalIDRepresentation)this.restConnector.post(path, IdentityMediaType.EXTERNAL_ID, representation);
        } else {
            throw new SDKException("Cannot determine global id value");
        }
    }

Anw, thank you very much.