diff --git a/bundles/org.opensmarthouse.core.auth.core/src/main/java/org/openhab/core/internal/auth/AuthorizationManagerImpl.java b/bundles/org.opensmarthouse.core.auth.core/src/main/java/org/openhab/core/internal/auth/AuthorizationManagerImpl.java new file mode 100644 index 000000000..68a9fe9b8 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth.core/src/main/java/org/openhab/core/internal/auth/AuthorizationManagerImpl.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.auth; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.Permission; +import org.openhab.core.auth.PermissionEvaluator; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component +public class AuthorizationManagerImpl implements AuthorizationManager { + + private final Logger logger = LoggerFactory.getLogger(AuthorizationManagerImpl.class); + private List> evaluators = new CopyOnWriteArrayList<>(); + + @Override + public boolean hasPermission(Permission permission, T object, Authentication authentication) { + if (authentication == null) { + return false; + } + + for (PermissionEvaluator evaluator : evaluators) { + if (evaluator.supports(object.getClass(), permission)) { + if (!evaluator.hasPermission(permission, authentication, object)) { + logger.trace("Access denied to object {} ({}) for authentication {} reported by evaluator {}", + object, object.getClass().getName(), authentication, evaluator); + return false; + } + } + } + return true; + } + + public boolean hasPermission(Permission permission, String id, Class type, Authentication authentication) { + if (authentication == null) { + return false; + } + + for (PermissionEvaluator evaluator : evaluators) { + if (evaluator.supports(type, permission)) { + if (!evaluator.hasPermission(permission, authentication, (Class) type, id)) { + logger.trace("Access denied to object with id {} ({}) for authentication {} reported by evaluator {}", + id, type.getName(), authentication, evaluator); + return false; + } + } + } + return true; + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void addPermissionEvaluator(PermissionEvaluator evaluator) { + this.evaluators.add(evaluator); + } + + public void removePermissionEvaluator(PermissionEvaluator evaluator) { + this.evaluators.remove(evaluator); + } + +} diff --git a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/GenericUser.java b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/GenericUser.java index ffac37d86..36c414ee2 100755 --- a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/GenericUser.java +++ b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/GenericUser.java @@ -12,6 +12,7 @@ */ package org.openhab.core.auth.local; +import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -28,16 +29,23 @@ public class GenericUser implements User { private String name; private Set roles; + private Set permissions; /** * Constructs a user attributed with a set of roles. * * @param name the username (account name) * @param roles the roles attributed to this user + * @param permissions the items user has access to */ - public GenericUser(String name, Set roles) { + public GenericUser(String name, Set roles, Set permissions) { this.name = name; this.roles = roles; + this.permissions = permissions; + } + + public GenericUser(String name, Set roles) { + this(name, roles, Collections.emptySet()); } /** @@ -64,7 +72,12 @@ public Set getRoles() { return roles; } + @Override + public Set getPermissions() { + return permissions; + } + public String toString() { - return name + " (" + roles + ")"; + return name + " (" + roles + " " + permissions + ")"; } } diff --git a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/ManagedUser.java b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/ManagedUser.java index a429d5194..42d3e22d9 100755 --- a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/ManagedUser.java +++ b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/ManagedUser.java @@ -19,7 +19,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.auth.local.User; /** * A {@link User} sourced from a managed {@link UserProvider}. @@ -33,6 +32,7 @@ public class ManagedUser implements User { private String passwordHash; private String passwordSalt; private Set roles = new HashSet<>(); + private Set permissions = new HashSet<>(); private @Nullable PendingToken pendingToken = null; private List sessions = new ArrayList<>(); private List apiTokens = new ArrayList<>(); @@ -120,6 +120,14 @@ public void setRoles(Set roles) { this.roles = roles; } + public void setPermissions(Set permissions) { + this.permissions = permissions; + } + + public Set getPermissions() { + return permissions; + } + /** * Gets the pending token information for this user, if any. * diff --git a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/User.java b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/User.java index fd78c1a44..2efd3eed7 100755 --- a/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/User.java +++ b/bundles/org.opensmarthouse.core.auth.local/src/main/java/org/openhab/core/auth/local/User.java @@ -33,5 +33,12 @@ public interface User extends Principal, Identifiable { * @see Role * @return role attributed to the user */ - public Set getRoles(); + Set getRoles(); + + /** + * Returns items which given user have access to. + * + * @return Item identifiers user has access to. + */ + Set getPermissions(); } diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Authentication.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Authentication.java index 9ebe3c0be..e94de3aef 100755 --- a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Authentication.java +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Authentication.java @@ -29,7 +29,7 @@ public class Authentication { private String username; private Set roles; private String scope; - private Set items; + private Set permissions; /** * no-args constructor required by gson @@ -38,7 +38,7 @@ protected Authentication() { this.username = null; this.roles = null; this.scope = null; - this.items = null; + this.permissions = null; } /** @@ -59,7 +59,7 @@ public Authentication(String username, String... roles) { * @param scope a scope this authentication is valid for */ public Authentication(String username, String[] roles, String scope) { - this(username, roles, scope, new String[] {"*"}); + this(username, roles, scope, new String[] {"*:*:*"}); } /** @@ -68,12 +68,13 @@ public Authentication(String username, String[] roles, String scope) { * @param username name of the user associated to this authentication instance * @param roles a variable list of roles that the user possesses. * @param scope a scope this authentication is valid for + * @param permissions permissions associated with authentication */ - public Authentication(String username, String[] roles, String scope, String[] items) { + public Authentication(String username, String[] roles, String scope, String[] permissions) { this.username = username; this.roles = Set.of(roles); this.scope = scope; - this.items = Set.of(items); + this.permissions = Set.of(permissions); } /** @@ -104,11 +105,11 @@ public String getScope() { } /** - * Retrieves the items this authentication is valid for. + * Retrieves the permissions this authentication has. * - * @return an set of items (might be empty) + * @return an set of permissions (might be empty) */ - public Set getItems() { - return items; + public Set getPermissions() { + return permissions; } } diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthenticationContextHolder.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthenticationContextHolder.java new file mode 100644 index 000000000..cc3ce07ef --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthenticationContextHolder.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +import java.util.Optional; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The access layer to an authentication context during execution of an action. + * + * @author Łukasz Dywicki - Initial contribution. + */ +public interface AuthenticationContextHolder { + + @Nullable + Authentication getAuthentication(); + + Optional fetchAuthentication(); + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationException.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationException.java new file mode 100644 index 000000000..652166ec3 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationException.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +/** + * The access layer to an authentication context during execution of an action. + * + * @author Łukasz Dywicki - Initial contribution. + */ +public class AuthorizationException extends Exception { + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManager.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManager.java new file mode 100755 index 000000000..3e3b3ad14 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManager.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +/** + * Authorization manager is main entry point for security checks. + * + * @author Łukasz Dywicki - Initial contribution + */ +public interface AuthorizationManager { + + boolean hasPermission(Permission permission, T object, Authentication authentication); + + boolean hasPermission(Permission permission, String id, Class type, Authentication authentication); + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManagerFilters.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManagerFilters.java new file mode 100644 index 000000000..101a4c6d7 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/AuthorizationManagerFilters.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +/** + * Helpers for filtering out collections and returning trimmed data. + * + * @author Łukasz Dywicki - Initial contribution + */ +public class AuthorizationManagerFilters { + + public static List filter(Authentication authentication, BiFunction check, List values) { + return values.stream().filter(value -> check.apply(authentication, value)) + .collect(Collectors.toList()); + } + + public static Set filter(Authentication authentication, BiFunction check, Set values) { + return values.stream().filter(value -> check.apply(authentication, value)) + .collect(Collectors.toSet()); + } + + public static Map filter(Authentication authentication, BiFunction, Boolean> check, Map values) { + return values.entrySet().stream().filter(entry -> check.apply(authentication, entry)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); + } + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/MutableAuthenticationContextHolder.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/MutableAuthenticationContextHolder.java new file mode 100644 index 000000000..984ec0d36 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/MutableAuthenticationContextHolder.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +/** + * Writeable holder for authentication context which should be used early in processing chain. + * + * @author Łukasz Dywicki - Initial contribution. + */ +public interface MutableAuthenticationContextHolder { + + void setAuthentication(Authentication authentication); + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/NamedPermission.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/NamedPermission.java new file mode 100644 index 000000000..2818820d6 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/NamedPermission.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +import java.util.Objects; + +/** + * Simplest possible implementation of permission. + * + * @author Łukasz Dywicki - Initial contribution. + */ +public class NamedPermission implements Permission { + + private final String code; + + public NamedPermission(String code) { + this.code = code; + } + + @Override + public String getCode() { + return code; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Permission)) { + return false; + } + Permission that = (Permission) o; + return Objects.equals(getCode(), that.getCode()); + } + + @Override + public int hashCode() { + return Objects.hash(getCode()); + } + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permission.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permission.java new file mode 100644 index 000000000..2514845d5 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permission.java @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +public interface Permission { + + String getCode(); + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/PermissionEvaluator.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/PermissionEvaluator.java new file mode 100644 index 000000000..09e6db0fa --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/PermissionEvaluator.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +public interface PermissionEvaluator { + + boolean supports(Class type, Permission permission); + + /** + * Decide if given value is accessible in given authentication context or not. + * + * @param permission Permission to be evaluated. + * @param authentication Authentication to verify. + * @param value Entity or resource to check. + * @return True if user can access given object. + */ + boolean hasPermission(Permission permission, Authentication authentication, T value); + + // same as above but uses type + id instead of entire object + boolean hasPermission(Permission permission, Authentication authentication, Class type, String id); + +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permissions.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permissions.java new file mode 100644 index 000000000..919471377 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/Permissions.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth; + +/** + * Common permission definitions. + * + * @author Łukasz Dywicki - Initial contribution + */ +public interface Permissions { + + Permission ALL = new NamedPermission("*"); + Permission READ = new NamedPermission("read"); + Permission STATE = new NamedPermission("state"); + Permission COMMAND = new NamedPermission("command"); + Permission MANAGE = new NamedPermission("manage"); + + Permission PERSISTENCE = new NamedPermission("persistence"); +} diff --git a/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/holder/ThreadLocalContextHolder.java b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/holder/ThreadLocalContextHolder.java new file mode 100644 index 000000000..81d8fa513 --- /dev/null +++ b/bundles/org.opensmarthouse.core.auth/src/main/java/org/openhab/core/auth/holder/ThreadLocalContextHolder.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.auth.holder; + +import java.util.Optional; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthenticationContextHolder; +import org.openhab.core.auth.MutableAuthenticationContextHolder; +import org.osgi.service.component.annotations.Component; + +/** + * Implementation of authentication context holder based on {@link ThreadLocal}. + * + * @author Łukasz Dywicki - Initial contribution. + */ +@Component(service = {AuthenticationContextHolder.class, MutableAuthenticationContextHolder.class}) +public class ThreadLocalContextHolder implements AuthenticationContextHolder, MutableAuthenticationContextHolder { + + private final ThreadLocal authentication = new ThreadLocal<>(); + + @Override + public Authentication getAuthentication() { + return authentication.get(); + } + + @Override + public Optional fetchAuthentication() { + return Optional.ofNullable(authentication.get()); + } + + // this one should be visible only to few + public void setAuthentication(Authentication authentication) { + this.authentication.set(authentication); + } + +} diff --git a/bundles/org.opensmarthouse.core.io.rest.auth.local/src/main/java/org/openhab/core/io/rest/auth/local/internal/JwtHelper.java b/bundles/org.opensmarthouse.core.io.rest.auth.local/src/main/java/org/openhab/core/io/rest/auth/local/internal/JwtHelper.java index 5c41fcac7..f9731cda0 100755 --- a/bundles/org.opensmarthouse.core.io.rest.auth.local/src/main/java/org/openhab/core/io/rest/auth/local/internal/JwtHelper.java +++ b/bundles/org.opensmarthouse.core.io.rest.auth.local/src/main/java/org/openhab/core/io/rest/auth/local/internal/JwtHelper.java @@ -117,7 +117,9 @@ public String getJwtAccessToken(User user, String clientId, String scope, int to jwtClaims.setClaim("client_id", clientId); jwtClaims.setClaim("scope", scope); jwtClaims.setStringListClaim("role", - new ArrayList<>(user.getRoles() != null ? user.getRoles() : Collections.emptySet())); + new ArrayList<>(user.getRoles() != null ? user.getRoles() : Collections.emptySet())); + jwtClaims.setStringListClaim("permissions", + new ArrayList<>(user.getPermissions() != null ? user.getPermissions() : Collections.emptySet())); JsonWebSignature jws = new JsonWebSignature(); jws.setPayload(jwtClaims.toJson()); @@ -149,10 +151,16 @@ public Authentication verifyAndParseJwtAccessToken(String jwt) throws Authentica JwtClaims jwtClaims = jwtConsumer.processToClaims(jwt); String username = jwtClaims.getSubject(); List roles = jwtClaims.getStringListClaimValue("role"); + List permissions = jwtClaims.getStringListClaimValue("permissions"); String scope = jwtClaims.getStringClaimValue("scope"); - return new Authentication(username, roles.toArray(new String[roles.size()]), scope); + return new Authentication(username, toArray(roles), scope, toArray(permissions)); } catch (InvalidJwtException | MalformedClaimException e) { throw new AuthenticationException("Error while processing JWT token", e); } } + + protected String[] toArray(List entries) { + return entries.toArray(new String[entries.size()]); + } + } diff --git a/bundles/org.opensmarthouse.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthzFilter.java b/bundles/org.opensmarthouse.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthzFilter.java new file mode 100644 index 000000000..d99422031 --- /dev/null +++ b/bundles/org.opensmarthouse.core.io.rest.auth/src/main/java/org/openhab/core/io/rest/auth/internal/AuthzFilter.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.auth.internal; + +import java.io.IOException; +import java.lang.reflect.Method; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; +import org.openhab.core.auth.MutableAuthenticationContextHolder; +import org.openhab.core.io.rest.RESTConstants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension; + +/** + * A propagation of security context through {@link org.openhab.core.auth.AuthenticationContextHolder}. + * + * @author Łukasz Dywicki - Initial contribution. + */ +@Component +@JaxrsExtension +@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")") +@Priority(Priorities.AUTHORIZATION) +@Provider +public class AuthzFilter implements ContainerRequestFilter, ContainerResponseFilter { + + private final MutableAuthenticationContextHolder authenticationContextHolder; + + @Activate + public AuthzFilter(@Reference MutableAuthenticationContextHolder authenticationContextHolder) { + this.authenticationContextHolder = authenticationContextHolder; + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + SecurityContext securityContext = requestContext.getSecurityContext(); + if (securityContext instanceof ApplicationSecurityContext) { + authenticationContextHolder.setAuthentication(((ApplicationSecurityContext) securityContext).getAuthentication()); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + authenticationContextHolder.setAuthentication(null); + } + +} \ No newline at end of file diff --git a/bundles/org.opensmarthouse.core.io.rest.item/pom.xml b/bundles/org.opensmarthouse.core.io.rest.item/pom.xml index 312e95607..ed442a8fd 100755 --- a/bundles/org.opensmarthouse.core.io.rest.item/pom.xml +++ b/bundles/org.opensmarthouse.core.io.rest.item/pom.xml @@ -51,6 +51,10 @@ org.osgi osgi.enroute.hamcrest.wrapper + + org.opensmarthouse.core.bundles + org.opensmarthouse.core.io.rest.auth + diff --git a/bundles/org.opensmarthouse.core.io.rest.item/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java b/bundles/org.opensmarthouse.core.io.rest.item/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java index 0c0b70e84..5aeee8b42 100755 --- a/bundles/org.opensmarthouse.core.io.rest.item/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java +++ b/bundles/org.opensmarthouse.core.io.rest.item/src/main/java/org/openhab/core/io/rest/core/internal/item/ItemResource.java @@ -47,6 +47,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthenticationContextHolder; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.Permissions; import org.openhab.core.auth.Role; import org.openhab.core.events.EventPublisher; import org.openhab.core.io.rest.DTOMapper; @@ -164,6 +168,8 @@ private static void respectForwarded(final UriBuilder uriBuilder, final @Context private final ManagedItemProvider managedItemProvider; private final MetadataRegistry metadataRegistry; private final MetadataSelectorMatcher metadataSelectorMatcher; + private final AuthorizationManager authorizationManager; + private final AuthenticationContextHolder authenticationContextHolder; @Activate public ItemResource(// @@ -174,7 +180,9 @@ public ItemResource(// final @Reference LocaleService localeService, // final @Reference ManagedItemProvider managedItemProvider, final @Reference MetadataRegistry metadataRegistry, - final @Reference MetadataSelectorMatcher metadataSelectorMatcher) { + final @Reference MetadataSelectorMatcher metadataSelectorMatcher, + final @Reference AuthorizationManager authorizationManager, + final @Reference AuthenticationContextHolder authenticationContextHolder) { this.dtoMapper = dtoMapper; this.eventPublisher = eventPublisher; this.itemBuilderFactory = itemBuilderFactory; @@ -183,6 +191,8 @@ public ItemResource(// this.managedItemProvider = managedItemProvider; this.metadataRegistry = metadataRegistry; this.metadataSelectorMatcher = metadataSelectorMatcher; + this.authorizationManager = authorizationManager; + this.authenticationContextHolder = authenticationContextHolder; } private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) { @@ -209,7 +219,9 @@ public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHead final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders); uriBuilder.path("{itemName}"); + final Authentication authentication = authenticationContextHolder.getAuthentication(); Stream itemStream = getItems(type, tags).stream() // + .filter(item -> authorizationManager.hasPermission(Permissions.READ, item, authentication)) .map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) // .peek(dto -> addMetadata(dto, namespaces, null)) // .peek(dto -> dto.editable = isEditable(dto.name)); @@ -228,6 +240,11 @@ public Response getItemData(final @Context UriInfo uriInfo, final @Context HttpH @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @QueryParam("metadata") @Parameter(description = "metadata selector") @Nullable String namespaceSelector, @PathParam("itemname") @Parameter(description = "item name") String itemname) { + + if (!authorizationManager.hasPermission(Permissions.READ, itemname, Item.class, authenticationContextHolder.getAuthentication())) { + return Response.status(Status.UNAUTHORIZED).build(); + } + final Locale locale = localeService.getLocale(language); final Set namespaces = splitAndFilterNamespaces(namespaceSelector, locale); @@ -262,6 +279,10 @@ private Set splitAndFilterNamespaces(@Nullable String namespaceSelector, @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse(responseCode = "404", description = "Item not found") }) public Response getPlainItemState(@PathParam("itemname") @Parameter(description = "item name") String itemname) { + if (!authorizationManager.hasPermission(Permissions.STATE, itemname, Item.class, authenticationContextHolder.getAuthentication())) { + return Response.status(Status.UNAUTHORIZED).build(); + } + // get item Item item = getItem(itemname); @@ -287,6 +308,11 @@ public Response putItemState( @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language, @PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "valid item state (e.g. ON, OFF)", required = true) String value) { + + if (!authorizationManager.hasPermission(Permissions.STATE, itemname, Item.class, authenticationContextHolder.getAuthentication())) { + return Response.status(Status.UNAUTHORIZED).build(); + } + final Locale locale = localeService.getLocale(language); // get Item @@ -321,6 +347,11 @@ public Response putItemState( @ApiResponse(responseCode = "400", description = "Item command null") }) public Response postItemCommand(@PathParam("itemname") @Parameter(description = "item name") String itemname, @Parameter(description = "valid item command (e.g. ON, OFF, UP, DOWN, REFRESH)", required = true) String value) { + + if (!authorizationManager.hasPermission(Permissions.COMMAND, itemname, Item.class, authenticationContextHolder.getAuthentication())) { + return Response.status(Status.UNAUTHORIZED).build(); + } + Item item = getItem(itemname); Command command = null; if (item != null) { @@ -718,7 +749,7 @@ private JsonObject buildStatusObject(String itemName, String status, @Nullable S /** * helper: Response to be sent to client if a Thing cannot be found * - * @param thingUID + * @param itemname * @return Response configured for 'item not found' */ private static Response getItemNotFoundResponse(String itemname) { diff --git a/bundles/org.opensmarthouse.core.io.rest.persistence/pom.xml b/bundles/org.opensmarthouse.core.io.rest.persistence/pom.xml index 0dd582731..5762a08c2 100755 --- a/bundles/org.opensmarthouse.core.io.rest.persistence/pom.xml +++ b/bundles/org.opensmarthouse.core.io.rest.persistence/pom.xml @@ -26,6 +26,10 @@ javax.annotation javax.annotation-api + + org.opensmarthouse.core.bundles + org.opensmarthouse.core.io.rest.auth + diff --git a/bundles/org.opensmarthouse.core.io.rest.persistence/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java b/bundles/org.opensmarthouse.core.io.rest.persistence/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java index bb8b317e8..e6e193775 100755 --- a/bundles/org.opensmarthouse.core.io.rest.persistence/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java +++ b/bundles/org.opensmarthouse.core.io.rest.persistence/src/main/java/org/openhab/core/io/rest/core/internal/persistence/PersistenceResource.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.function.BiFunction; import javax.annotation.security.RolesAllowed; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -37,6 +39,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthenticationContextHolder; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.AuthorizationManagerFilters; +import org.openhab.core.auth.Permissions; import org.openhab.core.auth.Role; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.rest.JSONResponse; @@ -53,6 +60,7 @@ import org.openhab.core.persistence.FilterCriteria.Ordering; import org.openhab.core.persistence.HistoricItem; import org.openhab.core.persistence.ModifiablePersistenceService; +import org.openhab.core.persistence.PersistenceItemInfo; import org.openhab.core.persistence.PersistenceService; import org.openhab.core.persistence.PersistenceServiceRegistry; import org.openhab.core.persistence.QueryablePersistenceService; @@ -115,17 +123,23 @@ public class PersistenceResource implements RESTResource { private final LocaleService localeService; private final PersistenceServiceRegistry persistenceServiceRegistry; private final TimeZoneProvider timeZoneProvider; + private final AuthorizationManager authorizationManager; + private final AuthenticationContextHolder authenticationContextHolder; @Activate public PersistenceResource( // final @Reference ItemRegistry itemRegistry, // final @Reference LocaleService localeService, final @Reference PersistenceServiceRegistry persistenceServiceRegistry, - final @Reference TimeZoneProvider timeZoneProvider) { + final @Reference TimeZoneProvider timeZoneProvider, + final @Reference AuthorizationManager authorizationManager, + final @Reference AuthenticationContextHolder authenticationContextHolder) { this.itemRegistry = itemRegistry; this.localeService = localeService; this.persistenceServiceRegistry = persistenceServiceRegistry; this.timeZoneProvider = timeZoneProvider; + this.authorizationManager = authorizationManager; + this.authenticationContextHolder = authenticationContextHolder; } @GET @@ -172,6 +186,10 @@ public Response httpGetPersistenceItemData(@Context HttpHeaders headers, @Parameter(description = "Page number of data to return. This parameter will enable paging.") @QueryParam("page") int pageNumber, @Parameter(description = "The length of each page.") @QueryParam("pagelength") int pageLength, @Parameter(description = "Gets one value before and after the requested period.") @QueryParam("boundary") boolean boundary) { + + if (!authorizationManager.hasPermission(Permissions.PERSISTENCE, itemName, Item.class, authenticationContextHolder.getAuthentication())) { + return Response.status(Status.UNAUTHORIZED).build(); + } return getItemHistoryDTO(serviceId, itemName, startTime, endTime, pageNumber, pageLength, boundary); } @@ -184,6 +202,7 @@ public Response httpGetPersistenceItemData(@Context HttpHeaders headers, @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = String.class)))), @ApiResponse(responseCode = "400", description = "Invalid filter parameters"), @ApiResponse(responseCode = "404", description = "Unknown persistence service") }) + public Response httpDeletePersistenceServiceItem(@Context HttpHeaders headers, @Parameter(description = "Id of the persistence service.", required = true) @QueryParam("serviceId") String serviceId, @Parameter(description = "The item name.") @PathParam("itemname") String itemName, @@ -361,7 +380,7 @@ private Response getItemHistoryDTO(@Nullable String serviceId, String itemName, /** * Gets a list of persistence services currently configured in the system * - * @return list of persistence services as {@link ServiceBean} + * @return list of persistence services as {@link PersistenceServiceDTO} */ private List getPersistenceServiceList(Locale locale) { List dtoList = new ArrayList<>(); @@ -407,7 +426,9 @@ private Response getServiceItemList(@Nullable String serviceId) { QueryablePersistenceService qService = (QueryablePersistenceService) service; - return JSONResponse.createResponse(Status.OK, qService.getItemInfo(), ""); + Set itemInfo = AuthorizationManagerFilters.filter(authenticationContextHolder.getAuthentication(), + new AuthorizationCheck(authorizationManager), qService.getItemInfo()); + return JSONResponse.createResponse(Status.OK, itemInfo, ""); } private Response deletePersistenceItemData(@Nullable String serviceId, String itemName, @Nullable String timeBegin, @@ -508,4 +529,18 @@ private Response putItemState(@Nullable String serviceId, String itemName, Strin mService.store(item, Date.from(dateTime.toInstant()), state); return Response.status(Status.OK).build(); } + + static class AuthorizationCheck implements BiFunction { + + private final AuthorizationManager authorizationManager; + + AuthorizationCheck(AuthorizationManager authorizationManager) { + this.authorizationManager = authorizationManager; + } + + @Override + public Boolean apply(Authentication auth, PersistenceItemInfo info) { + return authorizationManager.hasPermission(Permissions.PERSISTENCE, info.getName(), Item.class, auth); + } + } } \ No newline at end of file diff --git a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java index 6aee746e9..d86c19bff 100755 --- a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java +++ b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/SseResource.java @@ -12,16 +12,20 @@ */ package org.openhab.core.io.rest.sse; +import static org.openhab.core.io.rest.sse.internal.SseSinkItemInfo.canAccessItem; import static org.openhab.core.io.rest.sse.internal.SseSinkItemInfo.hasConnectionId; import static org.openhab.core.io.rest.sse.internal.SseSinkItemInfo.tracksItem; +import static org.openhab.core.io.rest.sse.internal.SseSinkTopicInfo.hasItemAccess; import static org.openhab.core.io.rest.sse.internal.SseSinkTopicInfo.matchesTopic; import java.io.IOException; +import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Predicate; import javax.annotation.security.RolesAllowed; import javax.inject.Singleton; import javax.servlet.http.HttpServletResponse; @@ -41,6 +45,10 @@ import javax.ws.rs.sse.SseEventSink; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.auth.AuthenticationContextHolder; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.Permission; +import org.openhab.core.auth.Permissions; import org.openhab.core.auth.Role; import org.openhab.core.events.Event; import org.openhab.core.io.rest.RESTConstants; @@ -52,6 +60,8 @@ import org.openhab.core.io.rest.sse.internal.SseSinkTopicInfo; import org.openhab.core.io.rest.sse.internal.dto.EventDTO; import org.openhab.core.io.rest.sse.internal.util.SseUtil; +import org.openhab.core.items.Item; +import org.openhab.core.items.events.ItemEvent; import org.openhab.core.items.events.ItemStateChangedEvent; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -104,11 +114,16 @@ public class SseResource implements RESTResource, SsePublisher { private final SseBroadcaster itemStatesBroadcaster = new SseBroadcaster<>(); private final SseItemStatesEventBuilder itemStatesEventBuilder; private final SseBroadcaster topicBroadcaster = new SseBroadcaster<>(); + private final AuthorizationManager authorizationManager; + private final AuthenticationContextHolder authenticationContextHolder; private ExecutorService executorService; @Activate - public SseResource(@Reference SseItemStatesEventBuilder itemStatesEventBuilder) { + public SseResource(@Reference SseItemStatesEventBuilder itemStatesEventBuilder, @Reference + AuthorizationManager authorizationManager, @Reference AuthenticationContextHolder authenticationContextHolder) { + this.authorizationManager = authorizationManager; + this.authenticationContextHolder = authenticationContextHolder; this.executorService = Executors.newSingleThreadExecutor(); this.itemStatesEventBuilder = itemStatesEventBuilder; } @@ -163,7 +178,7 @@ public void listen(@Context final SseEventSink sseEventSink, @Context final Http return; } - topicBroadcaster.add(sseEventSink, new SseSinkTopicInfo(eventFilter)); + topicBroadcaster.add(sseEventSink, new SseSinkTopicInfo(eventFilter, authenticationContextHolder.getAuthentication())); addCommonResponseHeaders(response); } @@ -172,7 +187,13 @@ private void handleEventBroadcastTopic(Event event) { final EventDTO eventDTO = SseUtil.buildDTO(event); final OutboundSseEvent sseEvent = SseUtil.buildEvent(sse.newEventBuilder(), eventDTO); - topicBroadcaster.sendIf(sseEvent, matchesTopic(eventDTO.topic)); + Predicate predicate = (sink) -> true; + if (event instanceof ItemEvent) { + String item = ((ItemEvent) event).getItemName(); + predicate = hasItemAccess(authorizationManager, item); + } + + topicBroadcaster.sendIf(sseEvent, matchesTopic(eventDTO.topic).and(predicate)); } /** @@ -187,7 +208,7 @@ private void handleEventBroadcastTopic(Event event) { @Operation(operationId = "initNewStateTacker", summary = "Initiates a new item state tracker connection", responses = { @ApiResponse(responseCode = "200", description = "OK") }) public void getStateEvents(@Context final SseEventSink sseEventSink, @Context final HttpServletResponse response) { - final SseSinkItemInfo sinkItemInfo = new SseSinkItemInfo(); + final SseSinkItemInfo sinkItemInfo = new SseSinkItemInfo(authenticationContextHolder.getAuthentication()); itemStatesBroadcaster.add(sseEventSink, sinkItemInfo); addCommonResponseHeaders(response); @@ -216,6 +237,13 @@ public Object updateTrackedItems(@PathParam("connectionId") String connectionId, return Response.status(Status.NOT_FOUND).build(); } + Set subsribedItemNames = new LinkedHashSet<>(); + for (String item : itemNames) { + if (authorizationManager.hasPermission(Permissions.READ, item, Item.class, authenticationContextHolder.getAuthentication())) { + subsribedItemNames.add(item); + } + } + itemStateInfo.get().updateTrackedItems(itemNames); OutboundSseEvent itemStateEvent = itemStatesEventBuilder.buildEvent(sse.newEventBuilder(), itemNames); @@ -233,7 +261,8 @@ public Object updateTrackedItems(@PathParam("connectionId") String connectionId, */ public void handleEventBroadcastItemState(final ItemStateChangedEvent stateChangeEvent) { String itemName = stateChangeEvent.getItemName(); - boolean isTracked = itemStatesBroadcaster.getInfoIf(info -> true).anyMatch(tracksItem(itemName)); + boolean isTracked = itemStatesBroadcaster.getInfoIf(info -> true).anyMatch(tracksItem(itemName) + .and(canAccessItem(authorizationManager, itemName))); if (isTracked) { OutboundSseEvent event = itemStatesEventBuilder.buildEvent(sse.newEventBuilder(), Set.of(itemName)); if (event != null) { diff --git a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkItemInfo.java b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkItemInfo.java index 560b4d263..aa315cc7c 100644 --- a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkItemInfo.java +++ b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkItemInfo.java @@ -18,6 +18,11 @@ import java.util.function.Predicate; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.Permissions; +import org.openhab.core.items.Item; /** * The specific information we need to hold for a SSE sink which tracks item state updates. @@ -29,6 +34,11 @@ public class SseSinkItemInfo { private final String connectionId = UUID.randomUUID().toString(); private final Set trackedItems = new CopyOnWriteArraySet<>(); + private final @Nullable Authentication authentication; + + public SseSinkItemInfo(@Nullable Authentication authentication) { + this.authentication = authentication; + } /** * Gets the connection identifier of this {@link SseSinkItemInfo} @@ -56,4 +66,13 @@ public static Predicate hasConnectionId(String connectionId) { public static Predicate tracksItem(String itemName) { return info -> info.trackedItems.contains(itemName); } + + public static Predicate canAccessItem(AuthorizationManager manager, String itemName) { + return info -> { + if (info.authentication == null) { + return false; + } + return manager.hasPermission(Permissions.READ, itemName, Item.class, info.authentication); + }; + } } diff --git a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkTopicInfo.java b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkTopicInfo.java index 1d11be620..2f11deae9 100644 --- a/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkTopicInfo.java +++ b/bundles/org.opensmarthouse.core.io.rest.sse/src/main/java/org/openhab/core/io/rest/sse/internal/SseSinkTopicInfo.java @@ -16,7 +16,12 @@ import java.util.function.Predicate; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.AuthorizationManager; +import org.openhab.core.auth.Permissions; import org.openhab.core.io.rest.sse.internal.util.SseUtil; +import org.openhab.core.items.Item; /** * The specific information we need to hold for a SSE sink which subscribes to event topics. @@ -27,12 +32,23 @@ public class SseSinkTopicInfo { private final List regexFilters; + private final @Nullable Authentication authentication; - public SseSinkTopicInfo(String topicFilter) { + public SseSinkTopicInfo(String topicFilter, @Nullable Authentication authentication) { this.regexFilters = SseUtil.convertToRegex(topicFilter); + this.authentication = authentication; } public static Predicate matchesTopic(final String topic) { return info -> info.regexFilters.stream().anyMatch(topic::matches); } + + public static Predicate hasItemAccess(final AuthorizationManager manager, final String item) { + return info -> { + if (info.authentication == null) { + return false; + } + return manager.hasPermission(Permissions.READ, item, Item.class, info.authentication); + }; + } } \ No newline at end of file diff --git a/bundles/org.opensmarthouse.core.io.rest/src/main/java/org/openhab/core/io/rest/Stream2JSONInputStream.java b/bundles/org.opensmarthouse.core.io.rest/src/main/java/org/openhab/core/io/rest/Stream2JSONInputStream.java index 50f9b6fca..09594bed8 100755 --- a/bundles/org.opensmarthouse.core.io.rest/src/main/java/org/openhab/core/io/rest/Stream2JSONInputStream.java +++ b/bundles/org.opensmarthouse.core.io.rest/src/main/java/org/openhab/core/io/rest/Stream2JSONInputStream.java @@ -17,6 +17,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Iterator; +import java.util.function.Predicate; import java.util.stream.Stream; import org.openhab.core.library.types.DateTimeType; @@ -41,6 +42,7 @@ public class Stream2JSONInputStream extends InputStream implements JSONInputStre private boolean firstIteratorElement; private final Gson gson = new GsonBuilder().setDateFormat(DateTimeType.DATE_PATTERN_WITH_TZ_AND_MS).create(); + private Predicate predicate; /** * Creates a new {@link Stream2JSONInputStream} backed by the given {@link Stream} source. @@ -53,11 +55,22 @@ public Stream2JSONInputStream(Stream source) { throw new IllegalArgumentException("The source must not be null!"); } - iterator = source.map(e -> gson.toJson(e)).iterator(); + iterator = source.filter(this::filter).map(e -> gson.toJson(e)).iterator(); jsonElementStream = new ByteArrayInputStream(new byte[0]); firstIteratorElement = true; } + public void setFilter(Predicate predicate) { + this.predicate = predicate; + } + + private boolean filter(Object element) { + if (this.predicate == null) { + return true; + } + return predicate.test(element); + } + @Override public int read() throws IOException { int result = jsonElementStream.read(); diff --git a/bundles/org.opensmarthouse.core.item.auth/pom.xml b/bundles/org.opensmarthouse.core.item.auth/pom.xml new file mode 100755 index 000000000..f692dc760 --- /dev/null +++ b/bundles/org.opensmarthouse.core.item.auth/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + + org.opensmarthouse.core.bundles + org.opensmarthouse.core.reactor.bundles + 0.9.2-SNAPSHOT + + + org.opensmarthouse.core.item.auth + + OpenSmartHouse Core | Bundles | Item | Auth + + + + org.opensmarthouse.core.bundles + org.opensmarthouse.core.item + + + org.opensmarthouse.core.bundles + org.opensmarthouse.core.auth + + + + diff --git a/bundles/org.opensmarthouse.core.item.auth/src/main/java/org/openhab/core/item/auth/internal/ItemPermissionEvaluator.java b/bundles/org.opensmarthouse.core.item.auth/src/main/java/org/openhab/core/item/auth/internal/ItemPermissionEvaluator.java new file mode 100644 index 000000000..e64742ca1 --- /dev/null +++ b/bundles/org.opensmarthouse.core.item.auth/src/main/java/org/openhab/core/item/auth/internal/ItemPermissionEvaluator.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2019-2020 Contributors to the OpenSmartHouse project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.item.auth.internal; + +import java.util.Set; +import org.openhab.core.auth.Authentication; +import org.openhab.core.auth.Permission; +import org.openhab.core.auth.PermissionEvaluator; +import org.openhab.core.auth.Permissions; +import org.openhab.core.auth.Role; +import org.openhab.core.items.Item; +import org.osgi.service.component.annotations.Component; + +@Component +public class ItemPermissionEvaluator implements PermissionEvaluator { + + @Override + public boolean supports(Class type, Permission permission) { + return Item.class.isAssignableFrom(type); + } + + @Override + public boolean hasPermission(Permission permission, Authentication authentication, Item value) { + if (authentication.getRoles().contains(Role.ADMIN)) { + return true; + } + + Set permissions = authentication.getPermissions(); + String item = value.getName(); + return evaluate(permission, permissions, item); + } + + @Override + public boolean hasPermission(Permission permission, Authentication authentication, Class type, String item) { + if (authentication.getRoles().contains(Role.ADMIN)) { + return true; + } + + Set permissions = authentication.getPermissions(); + return evaluate(permission, permissions, item); + } + + protected boolean evaluate(Permission permission, Set permissions, String item) { + if (permissions == null) { + return false; + } + + return isPermitted(permissions, permission, item) || + isPermitted(permissions, permission, "*") || + isPermitted(permissions, Permissions.ALL, item); + } + + private boolean isPermitted(Set permissions, Permission permission, String item) { + return permissions.contains(permission.getCode() + ":item:" + item); + } + +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 5f96c4a93..5caf6c5e4 100755 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -111,6 +111,7 @@ org.opensmarthouse.core.io.transport.serial.rxtx.rfc2217 org.opensmarthouse.core.io.transport.upnp org.opensmarthouse.core.item + org.opensmarthouse.core.item.auth org.opensmarthouse.core.item.core org.opensmarthouse.core.karaf org.opensmarthouse.core.karaf.jaas diff --git a/features/karaf/opensmarthouse-core/src/main/feature/feature.xml b/features/karaf/opensmarthouse-core/src/main/feature/feature.xml index a098681a6..7a5589552 100755 --- a/features/karaf/opensmarthouse-core/src/main/feature/feature.xml +++ b/features/karaf/opensmarthouse-core/src/main/feature/feature.xml @@ -314,6 +314,12 @@ mvn:org.opensmarthouse.core.bundles/org.opensmarthouse.core.auth.password/${project.version} + + opensmarthouse-core-auth + opensmarthouse-core-item + mvn:org.opensmarthouse.core.bundles/org.opensmarthouse.core.item.auth/${project.version} + + opensmarthouse-tp-gson opensmarthouse-core @@ -572,6 +578,7 @@ opensmarthouse-core-library-item opensmarthouse-core-io-rest + opensmarthouse-core-auth opensmarthouse-core-registry opensmarthouse-core-config opensmarthouse-core-thing @@ -582,6 +589,7 @@ opensmarthouse-tp-javax-inject opensmarthouse-core-io-rest + opensmarthouse-core-auth opensmarthouse-core-item opensmarthouse-core-transform mvn:org.opensmarthouse.core.bundles/org.opensmarthouse.core.io.rest.sse/${project.version} @@ -1082,6 +1090,7 @@ opensmarthouse-core-io-rest + opensmarthouse-core-auth opensmarthouse-core-persistence mvn:org.opensmarthouse.core.bundles/org.opensmarthouse.core.io.rest.persistence/${project.version}