All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openremote.manager.asset.AssetResourceImpl Maven / Gradle / Ivy

/*
 * Copyright 2016, OpenRemote Inc.
 *
 * See the CONTRIBUTORS.txt file in the distribution for a
 * full listing of individual contributors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see .
 */
package org.openremote.manager.asset;

import com.fasterxml.jackson.databind.node.NullNode;
import jakarta.persistence.OptimisticLockException;
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.plugins.validation.ResteasyViolationExceptionImpl;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebResource;
import org.openremote.model.Constants;
import org.openremote.model.asset.Asset;
import org.openremote.model.asset.AssetResource;
import org.openremote.model.asset.UserAssetLink;
import org.openremote.model.attribute.*;
import org.openremote.model.http.RequestParams;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.filter.RealmPredicate;
import org.openremote.model.security.ClientRole;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.ValueUtil;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.IntStream;

import static jakarta.ws.rs.core.Response.Status.*;
import static org.openremote.manager.asset.AssetProcessingService.ATTRIBUTE_EVENT_PROCESSOR;
import static org.openremote.model.query.AssetQuery.Access;
import static org.openremote.model.value.MetaItemType.*;

public class AssetResourceImpl extends ManagerWebResource implements AssetResource {

    private static final Logger LOG = Logger.getLogger(AssetResourceImpl.class.getName());
    protected final AssetStorageService assetStorageService;
    protected final MessageBrokerService messageBrokerService;
    protected final ClientEventService clientEventService;

    public AssetResourceImpl(TimerService timerService,
                             ManagerIdentityService identityService,
                             AssetStorageService assetStorageService,
                             MessageBrokerService messageBrokerService,
                             ClientEventService clientEventService) {
        super(timerService, identityService);
        this.assetStorageService = assetStorageService;
        this.messageBrokerService = messageBrokerService;
        this.clientEventService = clientEventService;
    }

    @Override
    public Asset[] getCurrentUserAssets(RequestParams requestParams) {
        try {
            if (isSuperUser()) {
                return new Asset[0];
            }

            if (!isAuthenticated()) {
                throw new NotAuthorizedException("Must be authenticated");
            }

            AssetQuery query = new AssetQuery().userIds(getUserId());

            if (!assetStorageService.authorizeAssetQuery(query, getAuthContext(), getRequestRealmName())) {
                throw new ForbiddenException("User not authorized to execute specified query");
            }

            List> assets = assetStorageService.findAll(query);

            // Compress response (the request attribute enables the interceptor)
            request.setAttribute(HttpHeaders.CONTENT_ENCODING, "gzip");

            return assets.toArray(new Asset[0]);
        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, BAD_REQUEST);
        }
    }

    @Override
    public UserAssetLink[] getUserAssetLinks(RequestParams requestParams, String realm, String userId, String assetId) {
        try {
            realm = TextUtil.isNullOrEmpty(realm) ? getAuthenticatedRealmName() : realm;
            boolean hasAdminReadRole = hasResourceRole(ClientRole.READ_ADMIN.getValue(), Constants.KEYCLOAK_CLIENT_ID);

            if (realm == null)
                throw new WebApplicationException(BAD_REQUEST);

            if (!(isSuperUser() || getAuthenticatedRealmName().equals(realm)))
                throw new WebApplicationException(FORBIDDEN);

            if (!hasAdminReadRole && userId != null && !Objects.equals(getUserId(), userId)) {
                throw new ForbiddenException("Can only retrieve own asset links unless you have role '" + ClientRole.READ_ADMIN + "'");
            }

            if (userId != null && !identityService.getIdentityProvider().isUserInRealm(userId, realm))
                throw new WebApplicationException(BAD_REQUEST);

            UserAssetLink[] result = assetStorageService.findUserAssetLinks(realm, userId, assetId).toArray(new UserAssetLink[0]);

            // Compress response (the request attribute enables the interceptor)
            request.setAttribute(HttpHeaders.CONTENT_ENCODING, "gzip");

            return result;

        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, BAD_REQUEST);
        }
    }


    @Override
    public void createUserAssetLinks(RequestParams requestParams, List userAssetLinks) {

        // Restricted users cannot create or delete links
        if (isRestrictedUser()) {
            throw new WebApplicationException(FORBIDDEN);
        }

        // Check all links are for the same user and realm
        String realm = userAssetLinks.get(0).getId().getRealm();
        String userId = userAssetLinks.get(0).getId().getUserId();
        String[] assetIds = new String[userAssetLinks.size()];

        IntStream.range(0, userAssetLinks.size()).forEach(i -> {
            UserAssetLink userAssetLink = userAssetLinks.get(i);
            assetIds[i] = userAssetLink.getId().getAssetId();

            if (!userAssetLink.getId().getRealm().equals(realm) || !userAssetLink.getId().getUserId().equals(userId)) {
                throw new BadRequestException("All user asset links must be for the same user");
            }
        });

        if (!isSuperUser() && !realm.equals(getAuthenticatedRealmName())) {
            throw new WebApplicationException(FORBIDDEN);
        }

        if (!identityService.getIdentityProvider().isUserInRealm(userId, realm)) {
            throw new WebApplicationException(FORBIDDEN);
        }

        List> assets = assetStorageService.findAll(
            new AssetQuery()
                .select(new AssetQuery.Select().excludeAttributes())
                .realm(new RealmPredicate(realm))
                .ids(assetIds)
        );

        if (assets.size() != userAssetLinks.size()) {
            throw new BadRequestException("One or more asset IDs are invalid");
        }

        try {
            assetStorageService.storeUserAssetLinks(userAssetLinks);
        } catch (Exception e) {
            throw new WebApplicationException(BAD_REQUEST);
        }
    }

    @Override
    public void deleteUserAssetLink(RequestParams requestParams, String realm, String userId, String assetId) {
        deleteUserAssetLinks(requestParams, Collections.singletonList(new UserAssetLink(realm, userId, assetId)));
    }

    @Override
    public void deleteAllUserAssetLinks(RequestParams requestParams, String realm, String userId) {
        // Restricted users cannot create or delete links
        if (isRestrictedUser()) {
            throw new WebApplicationException(FORBIDDEN);
        }

        // Regular users in a different realm can not delete links
        if (!isSuperUser() && !getAuthenticatedRealm().getName().equals(realm)) {
            throw new WebApplicationException(FORBIDDEN);
        }

        // User must be in the same realm as the requested realm
        if (!identityService.getIdentityProvider().isUserInRealm(userId, realm)) {
            throw new WebApplicationException(FORBIDDEN);
        }

        assetStorageService.deleteUserAssetLinks(userId);
    }

    @Override
    public void deleteUserAssetLinks(RequestParams requestParams, List userAssetLinks) {
        // Restricted users cannot create or delete links
        if (isRestrictedUser()) {
            throw new WebApplicationException(FORBIDDEN);
        }

        // Check all links are for the same user and realm
        String realm = userAssetLinks.get(0).getId().getRealm();
        String userId = userAssetLinks.get(0).getId().getUserId();

        if (userAssetLinks.stream().anyMatch(userAssetLink -> !userAssetLink.getId().getRealm().equals(realm) || !userAssetLink.getId().getUserId().equals(userId))) {
            throw new BadRequestException("All user asset links must be for the same user");
        }

        // Regular users in a different realm can not delete links
        if (!isSuperUser() && !getAuthenticatedRealm().getName().equals(realm)) {
            throw new WebApplicationException(FORBIDDEN);
        }

        // If delete count doesn't equal link count an exception will be thrown
        try {
            assetStorageService.deleteUserAssetLinks(userAssetLinks);
        } catch (Exception e) {
            LOG.log(Level.INFO, "Failed to delete user asset links", e);
            throw new BadRequestException();
        }
    }

    @Override
    public Asset getPartial(RequestParams requestParams, String assetId) {
        return get(requestParams, assetId, false);
    }

    @Override
    public Asset get(RequestParams requestParams, String assetId) {
        return get(requestParams, assetId, true);
    }

    public Asset get(RequestParams requestParams, String assetId, boolean loadComplete) {
        try {
            Asset asset;

            // Check restricted
            if (isRestrictedUser()) {
                if (!assetStorageService.isUserAsset(getUserId(), assetId)) {
                    LOG.fine("Forbidden access for restricted user: username=" + getUsername() + ", assetID=" + assetId);
                    throw new WebApplicationException(FORBIDDEN);
                }
                asset = assetStorageService.find(assetId, loadComplete, Access.PROTECTED);
            } else {
                asset = assetStorageService.find(assetId, loadComplete);
            }

            if (asset == null)
                throw new WebApplicationException(NOT_FOUND);

            if (!isRealmActiveAndAccessible(asset.getRealm())) {
                LOG.fine("Forbidden access (realm '" + asset.getRealm() + "' nonexistent, inactive or inaccessible) for user: " + getUsername());
                throw new WebApplicationException(FORBIDDEN);
            }

            // Compress response (the request attribute enables the interceptor)
            request.setAttribute(HttpHeaders.CONTENT_ENCODING, "gzip");

            return asset;

        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, BAD_REQUEST);
        }
    }

    @Override
    public Asset update(RequestParams requestParams, String assetId, Asset asset) {

        LOG.fine("Updating asset: assetID=" + assetId);

        try {
            Asset storageAsset = assetStorageService.find(assetId, true);

            if (storageAsset == null) {
                LOG.fine("Asset not found: assetID=" + assetId);
                throw new WebApplicationException(NOT_FOUND);
            }

            // Realm of asset must be accessible
            if (!isRealmActiveAndAccessible(storageAsset.getRealm())) {
                LOG.fine("Realm '" + storageAsset.getRealm() + "' is nonexistent, inactive or inaccessible: username=" + getUsername() + ", assetID=" + assetId);
                throw new WebApplicationException(FORBIDDEN);
            }

            if (!storageAsset.getRealm().equals(asset.getRealm())) {
                LOG.fine("Cannot change asset's realm: existingRealm=" + storageAsset.getRealm() + ", requestedRealm=" + asset.getRealm());
                throw new WebApplicationException(FORBIDDEN);
            }

            if (!storageAsset.getType().equals(asset.getType())) {
                LOG.fine("Cannot change asset's type: existingType=" + storageAsset.getType() + ", requestedType=" + asset.getType());
                throw new WebApplicationException(FORBIDDEN);
            }

            boolean isRestrictedUser = isRestrictedUser();

            // The asset that will ultimately be stored (override/ignore some values for restricted users)
            storageAsset.setVersion(asset.getVersion());

            if (!isRestrictedUser) {
                storageAsset.setName(asset.getName());
                storageAsset.setParentId(asset.getParentId());
                storageAsset.setAccessPublicRead(asset.isAccessPublicRead());
                storageAsset.setAttributes(asset.getAttributes());
            }

            // For restricted users, merge existing and updated attributes depending on write permissions
            if (isRestrictedUser) {

                if (!assetStorageService.isUserAsset(getUserId(), assetId)) {
                    throw new WebApplicationException(FORBIDDEN);
                }

                // Merge updated with existing attributes
                for (Attribute updatedAttribute : asset.getAttributes().values()) {

                    // Proper validation happens on merge(), here we only need the name to continue
                    String updatedAttributeName = updatedAttribute.getName();

                    // Check if attribute is present on the asset in storage
                    Optional> serverAttribute = storageAsset.getAttribute(updatedAttributeName);
                    if (serverAttribute.isPresent()) {
                        Attribute existingAttribute = serverAttribute.get();

                        // If the existing attribute is not writable by restricted client, ignore it
                        if (!existingAttribute.getMetaValue(ACCESS_RESTRICTED_WRITE).orElse(false)) {
                            LOG.fine("Existing attribute not writable by restricted client, ignoring update of: " + updatedAttributeName);
                            continue;
                        }

                        // Merge updated with existing meta items (modifying a copy)
                        MetaMap updatedMetaItems = updatedAttribute.getMeta();
                        // Ensure access meta is not modified
                        updatedMetaItems.removeIf(mi -> {
                            if (mi.getName().equals(ACCESS_RESTRICTED_READ.getName())) {
                                return true;
                            }
                            if (mi.getName().equals(ACCESS_RESTRICTED_WRITE.getName())) {
                                return true;
                            }
                            if (mi.getName().equals(ACCESS_PUBLIC_READ.getName())) {
                                return true;
                            }
                            if (mi.getName().equals(ACCESS_PUBLIC_WRITE.getName())) {
                                return true;
                            }
                            return false;
                        });

                        MetaMap existingMetaItems = ValueUtil.clone(existingAttribute.getMeta());

                        existingMetaItems.addOrReplace(updatedMetaItems);

                        // Replace existing with updated attribute
                        updatedAttribute.setMeta(existingMetaItems);
                        storageAsset.getAttributes().addOrReplace(updatedAttribute);

                    } else {

                        // An attribute added by a restricted user must be readable by restricted users
                        updatedAttribute.addOrReplaceMeta(new MetaItem<>(ACCESS_RESTRICTED_READ, true));

                        // An attribute added by a restricted user must be writable by restricted users
                        updatedAttribute.addOrReplaceMeta(new MetaItem<>(ACCESS_RESTRICTED_WRITE, true));

                        // Add the new attribute
                        storageAsset.getAttributes().addOrReplace(updatedAttribute);
                    }
                }

                // Remove missing attributes
                storageAsset.getAttributes().removeIf(existingAttribute ->
                        !asset.hasAttribute(existingAttribute.getName()) && existingAttribute.getMetaValue(ACCESS_RESTRICTED_WRITE).orElse(false)
                );
            }

//            // If attribute is type RULES_TEMPLATE_FILTER, enforce meta item RULE_STATE
//            // TODO Only done for update(Asset) and not create(Asset) as we don't need that right now
//            // TODO Implement "Saved Filter/Searches" properly, allowing restricted users to create rule state flags is not great
//            resultAsset .getAttributes().stream().forEach(attribute -> {
//                if (attribute.getType().map(attributeType -> attributeType == ValueType.RULES_TEMPLATE_FILTER).orElse(false)
//                    && !attribute.hasMetaItem(MetaItemType.RULE_STATE)) {
//                    attribute.addMeta(new MetaItem<>(MetaItemType.RULE_STATE, true));
//                }
//            });

            // Store the result
            return assetStorageService.merge(storageAsset, isRestrictedUser ? getUsername() : null);

        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, FORBIDDEN);
        } catch (ConstraintViolationException ex) {
            throw new ResteasyViolationExceptionImpl(ex.getConstraintViolations(), requestParams.headers.getAcceptableMediaTypes());
        } catch (OptimisticLockException opEx) {
            throw new WebApplicationException("Refresh the asset from the server and try to update the changes again", opEx, CONFLICT);
        }
    }

    @Override
    public Response writeAttributeValue(RequestParams requestParams, String assetId, String attributeName, Object value) {
        return writeAttributeValue(requestParams, assetId, attributeName, null, value);
    }

    @Override
    public Response writeAttributeValue(RequestParams requestParams, String assetId, String attributeName, Long timestamp, Object value) {
        Response.Status status = Response.Status.OK;

        if (value instanceof NullNode) {
            value = null;
        }

        AttributeEvent event = new AttributeEvent(assetId, attributeName, value, timestamp);

        // Check authorisation
        if (!clientEventService.authorizeEventWrite(getRequestRealmName(), getAuthContext(), event)) {
            throw new ForbiddenException("Cannot write specified attribute: " + event);
        }

        // Process asynchronously but block for a little while waiting for the result
        AttributeWriteResult result = doAttributeWrite(event);

        if (result.getFailure() != null) {
            status = switch (result.getFailure()) {
                case ASSET_NOT_FOUND, ATTRIBUTE_NOT_FOUND -> NOT_FOUND;
                case INVALID_VALUE -> NOT_ACCEPTABLE;
                case QUEUE_FULL -> TOO_MANY_REQUESTS;
                default -> BAD_REQUEST;
            };
        }

        return Response.status(status).entity(result).type(MediaType.APPLICATION_JSON_TYPE).build();
    }

    @Override
    public AttributeWriteResult[] writeAttributeValues(RequestParams requestParams, AttributeState[] attributeStates) {
        return writeAttributeEvents(requestParams,
                Arrays.stream(attributeStates)
                        .map(AttributeEvent::new)
                        .toArray(AttributeEvent[]::new)
        );
    }

    @Override
    public AttributeWriteResult[] writeAttributeEvents(RequestParams requestParams, AttributeEvent[] attributeEvents) {
        // Process asynchronously but block for a little while waiting for the result
        return Arrays.stream(attributeEvents).map(event -> {
            if (!clientEventService.authorizeEventWrite(getRequestRealmName(), getAuthContext(), event)) {
                return new AttributeWriteResult(event.getRef(), AttributeWriteFailure.INSUFFICIENT_ACCESS);
            }
            return doAttributeWrite(event);
        }).toArray(AttributeWriteResult[]::new);
    }

    @Override
    public Asset create(RequestParams requestParams, Asset asset) {
        try {
            if (isRestrictedUser()) {
                throw new WebApplicationException(FORBIDDEN);
            }

            if (asset == null) {
                LOG.finest("No asset in request");
                throw new WebApplicationException(BAD_REQUEST);
            }

            // If there was no realm provided (create was called by regular user in manager UI), use the auth realm
            if (asset.getRealm() == null || asset.getRealm().isEmpty()) {
                asset.setRealm(getAuthenticatedRealm().getName());
            } else if (!isRealmActiveAndAccessible(asset.getRealm())) {
                LOG.fine("Forbidden access for user '" + getUsername() + "', can't create: " + asset);
                throw new WebApplicationException(FORBIDDEN);
            }

            Asset newAsset = ValueUtil.clone(asset);

            // Allow client to set identifier
            if (asset.getId() != null) {
                newAsset.setId(asset.getId());
            }

            return assetStorageService.merge(newAsset);

        } catch (ConstraintViolationException ex) {
            throw new ResteasyViolationExceptionImpl(ex.getConstraintViolations(), requestParams.headers.getAcceptableMediaTypes());
        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, BAD_REQUEST);
        }
    }

    @Override
    public void delete(RequestParams requestParams, List assetIds) {

        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Deleting assets: " + assetIds);
        }

        try {
            if (assetIds == null || assetIds.isEmpty()) {
                throw new WebApplicationException(BAD_REQUEST);
            }

            if (isRestrictedUser()) {
                throw new WebApplicationException(FORBIDDEN);
            }

            List> assets = assetStorageService.findAll(new AssetQuery().ids(assetIds.toArray(new String[0])).select(new AssetQuery.Select().excludeAttributes()));
            if (assets == null || assets.size() != assetIds.size()) {
                LOG.fine("Request to delete one or more invalid assets");
                throw new WebApplicationException(BAD_REQUEST);
            }

            if (assets.stream().map(Asset::getRealm).distinct().anyMatch(asset -> !isRealmActiveAndAccessible(asset))) {
                LOG.fine("One or more assets in an nonexistent, inactive or inaccessible realm: username=" + getUsername());
                throw new WebApplicationException(FORBIDDEN);
            }

            if (!assetStorageService.delete(assetIds, false)) {
                throw new WebApplicationException(BAD_REQUEST);
            }
        } catch (IllegalStateException ex) {
            throw new WebApplicationException(ex, BAD_REQUEST);
        }
    }

    @Override
    public Asset[] queryAssets(RequestParams requestParams, AssetQuery query) {
        if (query == null) {
            query = new AssetQuery();
        }

        if (!assetStorageService.authorizeAssetQuery(query, getAuthContext(), getRequestRealmName())) {
            throw new ForbiddenException("User not authorized to execute specified query");
        }

        List> result = assetStorageService.findAll(query);

        // Compress response (the request attribute enables the interceptor)
        request.setAttribute(HttpHeaders.CONTENT_ENCODING, "gzip");

        return result.toArray(new Asset[0]);
    }

    protected AttributeWriteResult doAttributeWrite(AttributeEvent event) {
        AttributeWriteFailure failure = null;

        if (event.getTimestamp() <= 0) {
            event.setTimestamp(timerService.getCurrentTimeMillis());
        }

        try {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("Write attribute value request: " + event);
            }

            // Process synchronously - need to directly use the ATTRIBUTE_EVENT_QUEUE as the client inbound queue
            // has multiple consumers and so doesn't support In/Out MEP
            event.setSource(AssetResource.class.getSimpleName());
            Object result = messageBrokerService.getFluentProducerTemplate()
                .withBody(event)
                .to(ATTRIBUTE_EVENT_PROCESSOR)
                .request();

            if (result instanceof AssetProcessingException processingException) {
                failure = processingException.getReason();
            }

        } catch (AssetProcessingException e) {
            failure = e.getReason();
        } catch (IllegalStateException ex) {
            failure = AttributeWriteFailure.UNKNOWN;
        }

        return new AttributeWriteResult(event.getRef(), failure);
    }

    @Override
    public void updateParent(RequestParams requestParams, String parentId, List assetIds) {
        AssetQuery query = new AssetQuery();
        query.ids = assetIds.toArray(String[]::new);

        List> assets = this.assetStorageService.findAll(query);
        LOG.fine("Updating parent for assets: count=" + assets.size() + ", newParentID=" + parentId);

        for (Asset asset : assets) {
            asset.setParentId(parentId);
            LOG.fine("Updating asset parent: assetID=" + asset.getId() + ", newParentID=" + parentId);
            assetStorageService.merge(asset);
        }
    }

    @Override
    public void updateNoneParent(RequestParams requestParams, List assetIds) {
        AssetQuery query = new AssetQuery();
        query.ids = assetIds.toArray(String[]::new);

        List> assets = this.assetStorageService.findAll(query);
        LOG.fine("Updating parent for assets: count=" + assets.size() + ", newParentID=NONE");

        for (Asset asset : assets) {
            asset.setParentId(null);
            LOG.fine("Updating asset parent: assetID=" + asset.getId() + ", newParentID=NONE");
            assetStorageService.merge(asset);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy