Skip to content

Commit

Permalink
Support for public available dashboards (openremote#1054)
Browse files Browse the repository at this point in the history
Updates to dashboarding:
* Major improvements to the /dashboards HTTP API endpoints; doesn't require authentication anymore,
and now correctly checks whether a user has access to the data.
* Datapoints query endpoint now supports fetching datapoints of ACCESS_PUBLIC_READ assets.
* Added "open in new window" button to Insights page, to directly view dashboards in the Insights app.
* Added PUBLIC option to view access dropdown in dashboard settings.

Major update to the Insights standalone app:
* Removed initial login redirect, to allow viewing of public dashboards. Added login and logout button to menu instead. openremote#1046
* Support for realm switching within Insights app, by clicking "Change realm" button in the menu.
* Fix for 'master' logo not showing in menu. openremote#1047
* Automatic login redirect if no public dashboards could be found in that realm.
* Visiting dashboard of another realm as superuser, now automatically switches the realm.

Other fixes can be found in PR openremote#1054
  • Loading branch information
MartinaeyNL authored Jun 12, 2023
1 parent b2078ac commit 9d20afb
Show file tree
Hide file tree
Showing 27 changed files with 1,066 additions and 562 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package org.openremote.manager.dashboard;

import jakarta.ws.rs.WebApplicationException;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebResource;
import org.openremote.model.Constants;
import org.openremote.model.dashboard.Dashboard;
import org.openremote.model.dashboard.DashboardResource;
import org.openremote.model.dashboard.DashboardAccess;
import org.openremote.model.dashboard.DashboardResource;
import org.openremote.model.http.RequestParams;
import org.openremote.model.security.ClientRole;
import org.openremote.model.util.ValueUtil;

import jakarta.ws.rs.WebApplicationException;

import java.util.List;
import java.util.Collections;

import static jakarta.ws.rs.core.Response.Status.*;

Expand All @@ -30,31 +31,69 @@ public DashboardResourceImpl(TimerService timerService, ManagerIdentityService i

@Override
public Dashboard[] getAllRealmDashboards(RequestParams requestParams, String realm) {

boolean publicOnly = true;
if(realm == null) {
throw new WebApplicationException(BAD_REQUEST);
}
try {
// Realm should be enabled. Also takes unauthenticated users into count.
if(!isRealmActiveAndAccessible(realm)) {
throw new WebApplicationException(FORBIDDEN);
}
return this.dashboardStorageService.findAllOfRealm(realm, getUserId());
// if not authenticated, or having no INSIGHTS access, only fetch public dashboards
if(isAuthenticated()) {
publicOnly = (!hasResourceRole(ClientRole.READ_INSIGHTS.getValue(), Constants.KEYCLOAK_CLIENT_ID));
}
return this.dashboardStorageService.query(null, realm, getUserId(), publicOnly, false);

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

@Override
public Dashboard get(RequestParams requestParams, String dashboardId) {
return this.dashboardStorageService.get(dashboardId);
boolean publicOnly = true;
String realm = getRequestRealmName();
if(realm == null) {
throw new WebApplicationException(BAD_REQUEST);
}
try {
// Realm should be enabled. Also takes unauthenticated users into count.
if(!isRealmActiveAndAccessible(realm)) {
throw new WebApplicationException(FORBIDDEN);
}
// if not authenticated, or having no INSIGHTS access, only take public dashboards into count
if(isAuthenticated()) {
publicOnly = (!hasResourceRole(ClientRole.READ_INSIGHTS.getValue(), Constants.KEYCLOAK_CLIENT_ID));
}
Dashboard[] dashboards = this.dashboardStorageService.query(Collections.singletonList(dashboardId), realm, getUserId(), publicOnly, false);
if(dashboards.length == 0) {
if(this.dashboardStorageService.exists(dashboardId, realm)) {
throw new WebApplicationException(FORBIDDEN); // when no dashboard returned from query, but it does exist.
} else {
throw new WebApplicationException(NOT_FOUND); // aka it does not exist
}
}
return dashboards[0]; // only return first entry

} catch (IllegalStateException ex) {
ex.printStackTrace();
throw new WebApplicationException(ex, INTERNAL_SERVER_ERROR);
}
}

@Override
public Dashboard create(RequestParams requestParams, Dashboard dashboard) {
String realm = dashboard.getRealm();
if(realm == null) {
throw new WebApplicationException(BAD_REQUEST);
}
if(!isRealmActiveAndAccessible(dashboard.getRealm())) {
throw new WebApplicationException(FORBIDDEN);
}
try {

// Check if access to realm
if(!isRealmActiveAndAccessible(dashboard.getRealm())) {
throw new WebApplicationException(FORBIDDEN);
}
dashboard.setOwnerId(getUserId());
dashboard.setViewAccess(DashboardAccess.SHARED);
dashboard.setEditAccess(DashboardAccess.SHARED);
Expand All @@ -68,24 +107,37 @@ public Dashboard create(RequestParams requestParams, Dashboard dashboard) {
}

@Override
public void update(RequestParams requestParams, Dashboard dashboard) {
public Dashboard update(RequestParams requestParams, Dashboard dashboard) {
String realm = dashboard.getRealm();
if(realm == null) {
throw new WebApplicationException(BAD_REQUEST);
}
if(!isRealmActiveAndAccessible(realm)) {
throw new WebApplicationException(FORBIDDEN);
}
try {

// Check if access to realm
if(!isRealmActiveAndAccessible(dashboard.getRealm())) {
throw new WebApplicationException(FORBIDDEN);
}
this.dashboardStorageService.update(ValueUtil.clone(dashboard), getUserId());
return this.dashboardStorageService.update(ValueUtil.clone(dashboard), realm, getUserId());
} catch (IllegalArgumentException ex) {
throw new WebApplicationException(ex, NOT_FOUND);
} catch (IllegalStateException ex) {
ex.printStackTrace();
throw new WebApplicationException(ex, INTERNAL_SERVER_ERROR);
}
}

@Override
public void delete(RequestParams requestParams, List<String> fields) {
public void delete(RequestParams requestParams, String dashboardId) {
String realm = getRequestRealmName();
if(realm == null) {
throw new WebApplicationException(BAD_REQUEST);
}
if(!isRealmActiveAndAccessible(realm)) {
throw new WebApplicationException(FORBIDDEN);
}
try {
this.dashboardStorageService.delete(fields, getUserId());
this.dashboardStorageService.delete(dashboardId, realm, getUserId());
} catch (IllegalArgumentException ex) {
throw new WebApplicationException(ex, NOT_FOUND);
} catch (IllegalStateException ex) {
ex.printStackTrace();
throw new WebApplicationException(ex, INTERNAL_SERVER_ERROR);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.openremote.manager.dashboard;

import jakarta.persistence.Query;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
Expand All @@ -11,19 +16,7 @@
import org.openremote.model.dashboard.Dashboard;
import org.openremote.model.dashboard.DashboardAccess;

import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.ws.rs.WebApplicationException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
import java.util.*;

public class DashboardStorageService extends RouteBuilder implements ContainerService {

Expand Down Expand Up @@ -68,36 +61,50 @@ public void stop(Container container) throws Exception {

/* -------------------------- */

// Getting ALL dashboards from a realm
protected <T extends Dashboard> Dashboard[] findAllOfRealm(String realm, String userId) {
return this.findAllOfRealm(realm, userId, false);
}

@SuppressWarnings("java:S2326")
protected <T extends Dashboard> Dashboard[] findAllOfRealm(String realm, String userId, Boolean editable) {
// Querying dashboards from the database
// userId is required for checking dashboard ownership. If userId is NULL, we assume the user is not logged in.
// editable can be used to only return dashboards where the user has edit access.
protected Dashboard[] query(List<String> dashboardIds, String realm, String userId, Boolean publicOnly, Boolean editable) {
if(realm == null) {
throw new IllegalArgumentException("No realm is specified.");
}
return persistenceService.doReturningTransaction(em -> {
try {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Dashboard> cq = cb.createQuery(Dashboard.class);
Root<Dashboard> root = cq.from(Dashboard.class);

List<Predicate> predicates = new ArrayList<>();
if(realm != null) {
predicates.add(cb.like(root.get("realm"), realm));
predicates.add(cb.like(root.get("realm"), realm));

if(dashboardIds != null) {
predicates.add(root.get("id").in(dashboardIds));
}
if(editable) {
predicates.add(cb.or(
root.get("editAccess").in(DashboardAccess.PUBLIC, DashboardAccess.SHARED),
cb.and(root.get("editAccess").in(DashboardAccess.PRIVATE), root.get("ownerId").in(userId))
));
// Apply EDIT ACCESS filters; always return PUBLIC dashboards, SHARED dashboards if access to the realm,
// and PRIVATE if you are the creator (ownerId) of the dashboard.
if(Boolean.TRUE.equals(editable)) {
if(publicOnly) {
predicates.add(cb.equal(root.get("editAccess"), DashboardAccess.PUBLIC));
} else {
predicates.add(cb.or(
root.get("editAccess").in(DashboardAccess.PUBLIC, (userId != null ? DashboardAccess.SHARED : null)),
cb.and(root.get("editAccess").in(DashboardAccess.PRIVATE), root.get("ownerId").in(userId))
));
}
}
// Apply VIEW ACCESS filters; always return PUBLIC dashboards, SHARED dashboards if access to the realm,
// and PRIVATE if you are the creator (ownerId) of the dashboard.
if(publicOnly) {
predicates.add(cb.equal(root.get("viewAccess"), DashboardAccess.PUBLIC));
} else {
predicates.add(cb.or(
root.get("viewAccess").in(DashboardAccess.PUBLIC, DashboardAccess.SHARED),
root.get("viewAccess").in(DashboardAccess.PUBLIC, (userId != null ? DashboardAccess.SHARED : null)),
cb.and(root.get("viewAccess").in(DashboardAccess.PRIVATE), root.get("ownerId").in(userId))
));
}

CriteriaQuery<Dashboard> all = cq.select(root).where(predicates.toArray(new Predicate[]{}));
TypedQuery<Dashboard> allQuery = em.createQuery(all);
return allQuery.getResultList().toArray(new Dashboard[0]);
return em.createQuery(all).getResultList().toArray(new Dashboard[0]);

} catch (Exception e) {
e.printStackTrace();
Expand All @@ -106,14 +113,32 @@ protected <T extends Dashboard> Dashboard[] findAllOfRealm(String realm, String
});
}

@SuppressWarnings("java:S2326")
protected <T extends Dashboard> Dashboard get(String id) {
return persistenceService.doReturningTransaction(em -> em.find(Dashboard.class, id));
// Method to check if a dashboardId actually exists in the database
// Useful for when query() does not return any accessible dashboard for that user, and check if it does however exist.
protected boolean exists(String dashboardId, String realm) {
if(dashboardId == null) {
throw new IllegalArgumentException("No dashboardId is specified.");
}
if(realm == null) {
throw new IllegalArgumentException("No realm is specified.");
}
return persistenceService.doReturningTransaction(em -> {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Dashboard> cq = cb.createQuery(Dashboard.class);
Root<Dashboard> root = cq.from(Dashboard.class);
return em.createQuery(cq.select(root)
.where(cb.like(root.get("realm"), realm))
.where(cb.like(root.get("id"), dashboardId))
);
}) != null;
}


// Creation of initial dashboard (so no updating!)
protected <T extends Dashboard> T createNew(T dashboard) {
protected Dashboard createNew(Dashboard dashboard) {
if(dashboard == null) {
throw new IllegalArgumentException("No dashboard is specified.");
}
return persistenceService.doReturningTransaction(em -> {
if(dashboard.getId() != null && dashboard.getId().length() > 0) {
Dashboard d = em.find(Dashboard.class, dashboard.getId()); // checking whether dashboard is already in database
Expand All @@ -126,43 +151,49 @@ protected <T extends Dashboard> T createNew(T dashboard) {
}

// Update of an existing dashboard
protected <T extends Dashboard> T update(T dashboard, String userId) {
return persistenceService.doReturningTransaction(em -> {
Dashboard d = em.find(Dashboard.class, dashboard.getId());
if(d != null) {
if(d.getEditAccess() == DashboardAccess.PRIVATE) {
if(!(d.getOwnerId().equals(userId))) {
throw new WebApplicationException("You are not allowed to edit this dashboard!", FORBIDDEN);
}
}
dashboard.setVersion(d.getVersion()); // Always forcing to the correct version, no matter what.
protected Dashboard update(Dashboard dashboard, String realm, String userId) throws IllegalArgumentException {
if(dashboard == null) {
throw new IllegalArgumentException("No dashboard is specified.");
}
if(realm == null) {
throw new IllegalArgumentException("No realm is specified.");
}
if(userId == null) {
throw new IllegalArgumentException("No userId is specified.");
}
Dashboard[] dashboards = this.query(Collections.singletonList(dashboard.getId()), realm, userId, false, true); // Get dashboards that userId is able to EDIT.
if(dashboards != null && dashboards.length > 0) {
Dashboard d = dashboards[0];
return persistenceService.doReturningTransaction(em -> {
dashboard.setVersion(d.getVersion());
return em.merge(dashboard);
} else {
throw new WebApplicationException("This dashboard does not exist!", NOT_FOUND);
}
});
});
} else {
throw new IllegalArgumentException("This dashboard does not exist!");
}
}

protected boolean delete(List<String> dashboardIds, String userId) {
protected boolean delete(String dashboardId, String realm, String userId) throws IllegalArgumentException {
if(dashboardId == null) {
throw new IllegalArgumentException("No dashboardId is specified.");
}
if(realm == null) {
throw new IllegalArgumentException("No realm is specified.");
}
if(userId == null) {
throw new IllegalArgumentException("No userId is specified.");
}
return persistenceService.doReturningTransaction(em -> {
try {
Dashboard[] dashboards = this.findAllOfRealm(null, userId, true); // Get dashboards that userId is able to EDIT.
Collection<String> toDelete = new ArrayList<>();
for(Dashboard d : dashboards) {
if(dashboardIds.contains(d.getId())) {
toDelete.add(d.getId());
}
}
if(!toDelete.isEmpty()) {
Query query = em.createQuery("DELETE from Dashboard d WHERE d.id in (?1)");
query.setParameter(1, toDelete);
query.executeUpdate();
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;

// Query the dashboards with the same ID (which is only 1), and that userId is able to EDIT
Dashboard[] dashboards = this.query(Collections.singletonList(dashboardId), realm, userId, false, true);
if(dashboards == null || dashboards.length == 0) {
throw new IllegalArgumentException("No dashboards could be found.");
}
Query query = em.createQuery("DELETE from Dashboard d where d.id=?1");
query.setParameter(1, dashboardId);
query.executeUpdate();
return true;
});
}
}
Loading

0 comments on commit 9d20afb

Please sign in to comment.