diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java index d0c9bd6f3..dbdcaba94 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java @@ -41,7 +41,7 @@ import org.apache.guacamole.rest.language.LanguageRESTService; import org.apache.guacamole.rest.patch.PatchRESTService; import org.apache.guacamole.rest.schema.SchemaRESTService; import org.apache.guacamole.rest.tunnel.TunnelRESTService; -import org.apache.guacamole.rest.user.UserRESTService; +import org.apache.guacamole.rest.user.UserModule; /** * A Guice Module to set up the servlet mappings and authentication-specific @@ -94,7 +94,6 @@ public class RESTServiceModule extends ServletModule { bind(SchemaRESTService.class); bind(TokenRESTService.class); bind(TunnelRESTService.class); - bind(UserRESTService.class); // Root-level resources bind(SessionResource.class); @@ -104,6 +103,7 @@ public class RESTServiceModule extends ServletModule { install(new ActiveConnectionModule()); install(new ConnectionModule()); install(new ConnectionGroupModule()); + install(new UserModule()); // Set up the servlet and JSON mappings bind(GuiceContainer.class); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java index 9091e99cd..c9ff670e2 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/session/UserContextResource.java @@ -32,10 +32,12 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.ActiveConnection; import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.ConnectionGroup; +import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.rest.activeconnection.APIActiveConnection; import org.apache.guacamole.rest.connection.APIConnection; import org.apache.guacamole.rest.connectiongroup.APIConnectionGroup; +import org.apache.guacamole.rest.user.APIUser; /** * A REST resource which exposes the contents of a particular UserContext. @@ -75,6 +77,13 @@ public class UserContextResource { private DirectoryResourceFactory connectionGroupDirectoryResourceFactory; + /** + * Factory for creating DirectoryResources which expose a given + * User Directory. + */ + @Inject + private DirectoryResourceFactory userDirectoryResourceFactory; + /** * Creates a new UserContextResource which exposes the data within the * given UserContext. @@ -142,4 +151,22 @@ public class UserContextResource { userContext.getConnectionGroupDirectory()); } + /** + * Returns a new resource which represents the User Directory contained + * within the UserContext exposed by this UserContextResource. + * + * @return + * A new resource which represents the User Directory contained within + * the UserContext exposed by this UserContextResource. + * + * @throws GuacamoleException + * If an error occurs while retrieving the User Directory. + */ + @Path("users") + public DirectoryResource getUserDirectoryResource() + throws GuacamoleException { + return userDirectoryResourceFactory.create(userContext, + userContext.getUserDirectory()); + } + } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java new file mode 100644 index 000000000..a61bf05a9 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserDirectoryResource.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.rest.user; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.rest.directory.DirectoryObjectResource; +import org.apache.guacamole.rest.directory.DirectoryObjectResourceFactory; +import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; +import org.apache.guacamole.rest.directory.DirectoryResource; + +/** + * A REST resource which abstracts the operations available on a Directory of + * Users. + * + * @author Michael Jumper + */ +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class UserDirectoryResource extends DirectoryResource { + + /** + * The UserContext associated with the Directory which contains the + * User exposed by this resource. + */ + private final UserContext userContext; + + /** + * The Directory exposed by this resource. + */ + private final Directory directory; + + /** + * A factory which can be used to create instances of resources representing + * Users. + */ + private final DirectoryObjectResourceFactory resourceFactory; + + /** + * Creates a new UserDirectoryResource which exposes the operations and + * subresources available for the given User Directory. + * + * @param userContext + * The UserContext associated with the given Directory. + * + * @param directory + * The Directory being exposed. + * + * @param translator + * A DirectoryObjectTranslator implementation which handles + * Users. + * + * @param resourceFactory + * A factory which can be used to create instances of resources + * representing Users. + */ + @AssistedInject + public UserDirectoryResource(@Assisted UserContext userContext, + @Assisted Directory directory, + DirectoryObjectTranslator translator, + DirectoryObjectResourceFactory resourceFactory) { + super(userContext, directory, translator, resourceFactory); + this.userContext = userContext; + this.directory = directory; + this.resourceFactory = resourceFactory; + } + + @Override + public DirectoryObjectResource + getObjectResource(String identifier) throws GuacamoleException { + + // If username is own username, just use self - might not have query permissions + if (userContext.self().getIdentifier().equals(identifier)) + return resourceFactory.create(userContext, directory, userContext.self()); + + return super.getObjectResource(identifier); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserModule.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserModule.java new file mode 100644 index 000000000..5fa0774a2 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserModule.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.rest.user; + +import com.google.inject.AbstractModule; +import org.apache.guacamole.rest.directory.DirectoryObjectResourceFactory; +import org.apache.guacamole.rest.directory.DirectoryResourceFactory; +import com.google.inject.TypeLiteral; +import com.google.inject.assistedinject.FactoryModuleBuilder; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.rest.directory.DirectoryObjectResource; +import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; +import org.apache.guacamole.rest.directory.DirectoryResource; + +/** + * Guice Module which configures injections required for handling User resources + * via the REST API. + * + * @author Michael Jumper + */ +public class UserModule extends AbstractModule { + + @Override + protected void configure() { + + // Create the required DirectoryResourceFactory implementation + install(new FactoryModuleBuilder() + .implement( + new TypeLiteral>() {}, + UserDirectoryResource.class + ) + .build(new TypeLiteral>() {})); + + // Create the required DirectoryObjectResourceFactory implementation + install(new FactoryModuleBuilder() + .implement( + new TypeLiteral>() {}, + UserResource.class + ) + .build(new TypeLiteral>() {})); + + // Bind translator for converting between User and APIUser + bind(new TypeLiteral>() {}) + .to(UserObjectTranslator.class); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java new file mode 100644 index 000000000..5098c35fc --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserObjectTranslator.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.rest.user; + +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; + +/** + * Translator which converts between User objects and APIUser objects. + * + * @author Michael Jumper + */ +public class UserObjectTranslator + implements DirectoryObjectTranslator { + + @Override + public APIUser toExternalObject(User object) + throws GuacamoleException { + return new APIUser(object); + } + + @Override + public User toInternalObject(APIUser object) + throws GuacamoleException { + return new APIUserWrapper(object); + } + + @Override + public void applyExternalChanges(User existingObject, + APIUser object) throws GuacamoleException { + + // Do not update the user password if no password was provided + if (object.getPassword() != null) + existingObject.setPassword(object.getPassword()); + + // Update user attributes + existingObject.setAttributes(object.getAttributes()); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserRESTService.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserRESTService.java deleted file mode 100644 index c446ddf98..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserRESTService.java +++ /dev/null @@ -1,644 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.guacamole.rest.user; - -import com.google.inject.Inject; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.UUID; -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import org.apache.guacamole.GuacamoleClientException; -import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.GuacamoleResourceNotFoundException; -import org.apache.guacamole.GuacamoleSecurityException; -import org.apache.guacamole.net.auth.AuthenticationProvider; -import org.apache.guacamole.net.auth.Credentials; -import org.apache.guacamole.net.auth.Directory; -import org.apache.guacamole.net.auth.User; -import org.apache.guacamole.net.auth.UserContext; -import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; -import org.apache.guacamole.net.auth.permission.ObjectPermission; -import org.apache.guacamole.net.auth.permission.ObjectPermissionSet; -import org.apache.guacamole.net.auth.permission.Permission; -import org.apache.guacamole.net.auth.permission.SystemPermission; -import org.apache.guacamole.net.auth.permission.SystemPermissionSet; -import org.apache.guacamole.GuacamoleSession; -import org.apache.guacamole.rest.APIPatch; -import static org.apache.guacamole.rest.APIPatch.Operation.add; -import static org.apache.guacamole.rest.APIPatch.Operation.remove; -import org.apache.guacamole.rest.ObjectRetrievalService; -import org.apache.guacamole.rest.PATCH; -import org.apache.guacamole.rest.auth.AuthenticationService; -import org.apache.guacamole.rest.permission.APIPermissionSet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A REST Service for handling user CRUD operations. - * - * @author James Muehlner - * @author Michael Jumper - */ -@Path("/data/{dataSource}/users") -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -public class UserRESTService { - - /** - * Logger for this class. - */ - private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class); - - /** - * The prefix of any path within an operation of a JSON patch which - * modifies the permissions of a user regarding a specific connection. - */ - private static final String CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/connectionPermissions/"; - - /** - * The prefix of any path within an operation of a JSON patch which - * modifies the permissions of a user regarding a specific connection group. - */ - private static final String CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX = "/connectionGroupPermissions/"; - - /** - * The prefix of any path within an operation of a JSON patch which - * modifies the permissions of a user regarding a specific active connection. - */ - private static final String ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX = "/activeConnectionPermissions/"; - - /** - * The prefix of any path within an operation of a JSON patch which - * modifies the permissions of a user regarding another, specific user. - */ - private static final String USER_PERMISSION_PATCH_PATH_PREFIX = "/userPermissions/"; - - /** - * The path of any operation within a JSON patch which modifies the - * permissions of a user regarding the entire system. - */ - private static final String SYSTEM_PERMISSION_PATCH_PATH = "/systemPermissions"; - - /** - * A service for authenticating users from auth tokens. - */ - @Inject - private AuthenticationService authenticationService; - - /** - * Service for convenient retrieval of objects. - */ - @Inject - private ObjectRetrievalService retrievalService; - - /** - * Gets a list of users in the given data source (UserContext), filtering - * the returned list by the given permission, if specified. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext from which the users are to be retrieved. - * - * @param permissions - * The set of permissions to filter with. A user must have one or more - * of these permissions for a user to appear in the result. - * If null, no filtering will be performed. - * - * @return - * A list of all visible users. If a permission was specified, this - * list will contain only those users for whom the current user has - * that permission. - * - * @throws GuacamoleException - * If an error is encountered while retrieving users. - */ - @GET - public List getUsers(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @QueryParam("permission") List permissions) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // An admin user has access to any user - User self = userContext.self(); - SystemPermissionSet systemPermissions = self.getSystemPermissions(); - boolean isAdmin = systemPermissions.hasPermission(SystemPermission.Type.ADMINISTER); - - // Get the directory - Directory userDirectory = userContext.getUserDirectory(); - - // Filter users, if requested - Collection userIdentifiers = userDirectory.getIdentifiers(); - if (!isAdmin && permissions != null && !permissions.isEmpty()) { - ObjectPermissionSet userPermissions = self.getUserPermissions(); - userIdentifiers = userPermissions.getAccessibleObjects(permissions, userIdentifiers); - } - - // Retrieve all users, converting to API users - List apiUsers = new ArrayList(); - for (User user : userDirectory.getAll(userIdentifiers)) - apiUsers.add(new APIUser(user)); - - return apiUsers; - - } - - /** - * Retrieves an individual user. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext from which the requested user is to be retrieved. - * - * @param username - * The username of the user to retrieve. - * - * @return user - * The user having the given username. - * - * @throws GuacamoleException - * If an error occurs while retrieving the user. - */ - @GET - @Path("/{username}") - public APIUser getUser(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - - // Retrieve the requested user - User user = retrievalService.retrieveUser(session, authProviderIdentifier, username); - return new APIUser(user); - - } - - /** - * Creates a new user and returns the user that was created. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext in which the requested user is to be created. - * - * @param user - * The new user to create. - * - * @throws GuacamoleException - * If a problem is encountered while creating the user. - * - * @return - * The newly created user. - */ - @POST - public APIUser createUser(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, APIUser user) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // Get the directory - Directory userDirectory = userContext.getUserDirectory(); - - // Randomly set the password if it wasn't provided - if (user.getPassword() == null) - user.setPassword(UUID.randomUUID().toString()); - - // Create the user - userDirectory.add(new APIUserWrapper(user)); - - return user; - - } - - /** - * Updates an individual existing user. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext in which the requested user is to be updated. - * - * @param username - * The username of the user to update. - * - * @param user - * The data to update the user with. - * - * @throws GuacamoleException - * If an error occurs while updating the user. - */ - @PUT - @Path("/{username}") - public void updateUser(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username, APIUser user) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // Get the directory - Directory userDirectory = userContext.getUserDirectory(); - - // Validate data and path are sane - if (!user.getUsername().equals(username)) - throw new GuacamoleClientException("Username in path does not match username provided JSON data."); - - // A user may not use this endpoint to modify himself - if (userContext.self().getIdentifier().equals(user.getUsername())) - throw new GuacamoleSecurityException("Permission denied."); - - // Get the user - User existingUser = retrievalService.retrieveUser(userContext, username); - - // Do not update the user password if no password was provided - if (user.getPassword() != null) - existingUser.setPassword(user.getPassword()); - - // Update user attributes - existingUser.setAttributes(user.getAttributes()); - - // Update the user - userDirectory.update(existingUser); - - } - - /** - * Updates the password for an individual existing user. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext in which the requested user is to be updated. - * - * @param username - * The username of the user to update. - * - * @param userPasswordUpdate - * The object containing the old password for the user, as well as the - * new password to set for that user. - * - * @param request - * The HttpServletRequest associated with the password update attempt. - * - * @throws GuacamoleException - * If an error occurs while updating the user's password. - */ - @PUT - @Path("/{username}/password") - public void updatePassword(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username, - APIUserPasswordUpdate userPasswordUpdate, - @Context HttpServletRequest request) throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // Build credentials - Credentials credentials = new Credentials(); - credentials.setUsername(username); - credentials.setPassword(userPasswordUpdate.getOldPassword()); - credentials.setRequest(request); - credentials.setSession(request.getSession(true)); - - // Verify that the old password was correct - try { - AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); - if (authProvider.authenticateUser(credentials) == null) - throw new GuacamoleSecurityException("Permission denied."); - } - - // Pass through any credentials exceptions as simple permission denied - catch (GuacamoleCredentialsException e) { - throw new GuacamoleSecurityException("Permission denied."); - } - - // Get the user directory - Directory userDirectory = userContext.getUserDirectory(); - - // Get the user that we want to updates - User user = retrievalService.retrieveUser(userContext, username); - - // Set password to the newly provided one - user.setPassword(userPasswordUpdate.getNewPassword()); - - // Update the user - userDirectory.update(user); - - } - - /** - * Deletes an individual existing user. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext from which the requested user is to be deleted. - * - * @param username - * The username of the user to delete. - * - * @throws GuacamoleException - * If an error occurs while deleting the user. - */ - @DELETE - @Path("/{username}") - public void deleteUser(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // Get the directory - Directory userDirectory = userContext.getUserDirectory(); - - // Get the user - User existingUser = userDirectory.get(username); - if (existingUser == null) - throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); - - // Delete the user - userDirectory.remove(username); - - } - - /** - * Gets a list of permissions for the user with the given username. - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext in which the requested user is to be found. - * - * @param username - * The username of the user to retrieve permissions for. - * - * @return - * A list of all permissions granted to the specified user. - * - * @throws GuacamoleException - * If an error occurs while retrieving permissions. - */ - @GET - @Path("/{username}/permissions") - public APIPermissionSet getPermissions(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username) - throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - User user; - - // If username is own username, just use self - might not have query permissions - if (userContext.self().getIdentifier().equals(username)) - user = userContext.self(); - - // If not self, query corresponding user from directory - else { - user = userContext.getUserDirectory().get(username); - if (user == null) - throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); - } - - return new APIPermissionSet(user); - - } - - /** - * Updates the given permission set patch by queuing an add or remove - * operation for the given permission based on the given patch operation. - * - * @param - * The type of permission stored within the permission set. - * - * @param operation - * The patch operation to perform. - * - * @param permissionSetPatch - * The permission set patch being modified. - * - * @param permission - * The permission being added or removed from the set. - * - * @throws GuacamoleException - * If the requested patch operation is not supported. - */ - private void updatePermissionSet( - APIPatch.Operation operation, - PermissionSetPatch permissionSetPatch, - PermissionType permission) throws GuacamoleException { - - // Add or remove permission based on operation - switch (operation) { - - // Add permission - case add: - permissionSetPatch.addPermission(permission); - break; - - // Remove permission - case remove: - permissionSetPatch.removePermission(permission); - break; - - // Unsupported patch operation - default: - throw new GuacamoleClientException("Unsupported patch operation: \"" + operation + "\""); - - } - - } - - /** - * Applies a given list of permission patches. Each patch specifies either - * an "add" or a "remove" operation for a permission type, represented by - * a string. Valid permission types depend on the path of each patch - * operation, as the path dictates the permission being modified, such as - * "/connectionPermissions/42" or "/systemPermissions". - * - * @param authToken - * The authentication token that is used to authenticate the user - * performing the operation. - * - * @param authProviderIdentifier - * The unique identifier of the AuthenticationProvider associated with - * the UserContext in which the requested user is to be found. - * - * @param username - * The username of the user to modify the permissions of. - * - * @param patches - * The permission patches to apply for this request. - * - * @throws GuacamoleException - * If a problem is encountered while modifying permissions. - */ - @PATCH - @Path("/{username}/permissions") - public void patchPermissions(@QueryParam("token") String authToken, - @PathParam("dataSource") String authProviderIdentifier, - @PathParam("username") String username, - List> patches) throws GuacamoleException { - - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - UserContext userContext = retrievalService.retrieveUserContext(session, authProviderIdentifier); - - // Get the user - User user = userContext.getUserDirectory().get(username); - if (user == null) - throw new GuacamoleResourceNotFoundException("No such user: \"" + username + "\""); - - // Permission patches for all types of permissions - PermissionSetPatch connectionPermissionPatch = new PermissionSetPatch(); - PermissionSetPatch connectionGroupPermissionPatch = new PermissionSetPatch(); - PermissionSetPatch activeConnectionPermissionPatch = new PermissionSetPatch(); - PermissionSetPatch userPermissionPatch = new PermissionSetPatch(); - PermissionSetPatch systemPermissionPatch = new PermissionSetPatch(); - - // Apply all patch operations individually - for (APIPatch patch : patches) { - - String path = patch.getPath(); - - // Create connection permission if path has connection prefix - if (path.startsWith(CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { - - // Get identifier and type from patch operation - String identifier = path.substring(CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); - ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); - - // Create and update corresponding permission - ObjectPermission permission = new ObjectPermission(type, identifier); - updatePermissionSet(patch.getOp(), connectionPermissionPatch, permission); - - } - - // Create connection group permission if path has connection group prefix - else if (path.startsWith(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX)) { - - // Get identifier and type from patch operation - String identifier = path.substring(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX.length()); - ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); - - // Create and update corresponding permission - ObjectPermission permission = new ObjectPermission(type, identifier); - updatePermissionSet(patch.getOp(), connectionGroupPermissionPatch, permission); - - } - - // Create active connection permission if path has active connection prefix - else if (path.startsWith(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { - - // Get identifier and type from patch operation - String identifier = path.substring(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); - ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); - - // Create and update corresponding permission - ObjectPermission permission = new ObjectPermission(type, identifier); - updatePermissionSet(patch.getOp(), activeConnectionPermissionPatch, permission); - - } - - // Create user permission if path has user prefix - else if (path.startsWith(USER_PERMISSION_PATCH_PATH_PREFIX)) { - - // Get identifier and type from patch operation - String identifier = path.substring(USER_PERMISSION_PATCH_PATH_PREFIX.length()); - ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); - - // Create and update corresponding permission - ObjectPermission permission = new ObjectPermission(type, identifier); - updatePermissionSet(patch.getOp(), userPermissionPatch, permission); - - } - - // Create system permission if path is system path - else if (path.equals(SYSTEM_PERMISSION_PATCH_PATH)) { - - // Get identifier and type from patch operation - SystemPermission.Type type = SystemPermission.Type.valueOf(patch.getValue()); - - // Create and update corresponding permission - SystemPermission permission = new SystemPermission(type); - updatePermissionSet(patch.getOp(), systemPermissionPatch, permission); - - } - - // Otherwise, the path is not supported - else - throw new GuacamoleClientException("Unsupported patch path: \"" + path + "\""); - - } // end for each patch operation - - // Save the permission changes - connectionPermissionPatch.apply(user.getConnectionPermissions()); - connectionGroupPermissionPatch.apply(user.getConnectionGroupPermissions()); - activeConnectionPermissionPatch.apply(user.getActiveConnectionPermissions()); - userPermissionPatch.apply(user.getUserPermissions()); - systemPermissionPatch.apply(user.getSystemPermissions()); - - } - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java new file mode 100644 index 000000000..77009b6ca --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/user/UserResource.java @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.guacamole.rest.user; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import org.apache.guacamole.GuacamoleClientException; +import org.apache.guacamole.GuacamoleException; +import org.apache.guacamole.GuacamoleSecurityException; +import org.apache.guacamole.net.auth.AuthenticationProvider; +import org.apache.guacamole.net.auth.Credentials; +import org.apache.guacamole.net.auth.User; +import org.apache.guacamole.net.auth.Directory; +import org.apache.guacamole.net.auth.UserContext; +import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; +import org.apache.guacamole.net.auth.permission.ObjectPermission; +import org.apache.guacamole.net.auth.permission.Permission; +import org.apache.guacamole.net.auth.permission.SystemPermission; +import org.apache.guacamole.rest.APIPatch; +import org.apache.guacamole.rest.PATCH; +import org.apache.guacamole.rest.directory.DirectoryObjectResource; +import org.apache.guacamole.rest.directory.DirectoryObjectTranslator; +import org.apache.guacamole.rest.permission.APIPermissionSet; + +/** + * A REST resource which abstracts the operations available on an existing + * User. + * + * @author Michael Jumper + */ +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class UserResource + extends DirectoryObjectResource { + + /** + * The UserContext associated with the Directory which contains the User + * exposed by this resource. + */ + private final UserContext userContext; + + /** + * The Directory which contains the User object represented by this + * UserResource. + */ + private final Directory directory; + + /** + * The User object represented by this UserResource. + */ + private final User user; + + /** + * Creates a new UserResource which exposes the operations and subresources + * available for the given User. + * + * @param userContext + * The UserContext associated with the given Directory. + * + * @param directory + * The Directory which contains the given User. + * + * @param user + * The User that this UserResource should represent. + * + * @param translator + * A DirectoryObjectTranslator implementation which handles Users. + */ + @AssistedInject + public UserResource(@Assisted UserContext userContext, + @Assisted Directory directory, + @Assisted User user, + DirectoryObjectTranslator translator) { + super(directory, user, translator); + this.userContext = userContext; + this.directory = directory; + this.user = user; + } + + @Override + public void updateObject(APIUser modifiedObject) throws GuacamoleException { + + // A user may not use this endpoint to modify himself + if (userContext.self().getIdentifier().equals(modifiedObject.getUsername())) + throw new GuacamoleSecurityException("Permission denied."); + + super.updateObject(modifiedObject); + + } + + /** + * Updates the password for an individual existing user. + * + * @param userPasswordUpdate + * The object containing the old password for the user, as well as the + * new password to set for that user. + * + * @param request + * The HttpServletRequest associated with the password update attempt. + * + * @throws GuacamoleException + * If an error occurs while updating the user's password. + */ + @PUT + @Path("password") + public void updatePassword(APIUserPasswordUpdate userPasswordUpdate, + @Context HttpServletRequest request) throws GuacamoleException { + + // Build credentials + Credentials credentials = new Credentials(); + credentials.setUsername(user.getIdentifier()); + credentials.setPassword(userPasswordUpdate.getOldPassword()); + credentials.setRequest(request); + credentials.setSession(request.getSession(true)); + + // Verify that the old password was correct + try { + AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); + if (authProvider.authenticateUser(credentials) == null) + throw new GuacamoleSecurityException("Permission denied."); + } + + // Pass through any credentials exceptions as simple permission denied + catch (GuacamoleCredentialsException e) { + throw new GuacamoleSecurityException("Permission denied."); + } + + // Set password to the newly provided one + user.setPassword(userPasswordUpdate.getNewPassword()); + directory.update(user); + + } + + /** + * Gets a list of permissions for the user with the given username. + * + * @return + * A list of all permissions granted to the specified user. + * + * @throws GuacamoleException + * If an error occurs while retrieving permissions. + */ + @GET + @Path("permissions") + public APIPermissionSet getPermissions() throws GuacamoleException { + return new APIPermissionSet(user); + } + + /** + * Updates the given permission set patch by queuing an add or remove + * operation for the given permission based on the given patch operation. + * + * @param + * The type of permission stored within the permission set. + * + * @param operation + * The patch operation to perform. + * + * @param permissionSetPatch + * The permission set patch being modified. + * + * @param permission + * The permission being added or removed from the set. + * + * @throws GuacamoleException + * If the requested patch operation is not supported. + */ + private void updatePermissionSet( + APIPatch.Operation operation, + PermissionSetPatch permissionSetPatch, + PermissionType permission) throws GuacamoleException { + + // Add or remove permission based on operation + switch (operation) { + + // Add permission + case add: + permissionSetPatch.addPermission(permission); + break; + + // Remove permission + case remove: + permissionSetPatch.removePermission(permission); + break; + + // Unsupported patch operation + default: + throw new GuacamoleClientException("Unsupported patch operation: \"" + operation + "\""); + + } + + } + + /** + * Applies a given list of permission patches. Each patch specifies either + * an "add" or a "remove" operation for a permission type, represented by + * a string. Valid permission types depend on the path of each patch + * operation, as the path dictates the permission being modified, such as + * "/connectionPermissions/42" or "/systemPermissions". + * + * @param patches + * The permission patches to apply for this request. + * + * @throws GuacamoleException + * If a problem is encountered while modifying permissions. + */ + @PATCH + @Path("permissions") + public void patchPermissions(List> patches) + throws GuacamoleException { + + // Permission patches for all types of permissions + PermissionSetPatch connectionPermissionPatch = new PermissionSetPatch(); + PermissionSetPatch connectionGroupPermissionPatch = new PermissionSetPatch(); + PermissionSetPatch activeConnectionPermissionPatch = new PermissionSetPatch(); + PermissionSetPatch userPermissionPatch = new PermissionSetPatch(); + PermissionSetPatch systemPermissionPatch = new PermissionSetPatch(); + + // Apply all patch operations individually + for (APIPatch patch : patches) { + + String path = patch.getPath(); + + // Create connection permission if path has connection prefix + if (path.startsWith(CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create and update corresponding permission + ObjectPermission permission = new ObjectPermission(type, identifier); + updatePermissionSet(patch.getOp(), connectionPermissionPatch, permission); + + } + + // Create connection group permission if path has connection group prefix + else if (path.startsWith(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(CONNECTION_GROUP_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create and update corresponding permission + ObjectPermission permission = new ObjectPermission(type, identifier); + updatePermissionSet(patch.getOp(), connectionGroupPermissionPatch, permission); + + } + + // Create active connection permission if path has active connection prefix + else if (path.startsWith(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(ACTIVE_CONNECTION_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create and update corresponding permission + ObjectPermission permission = new ObjectPermission(type, identifier); + updatePermissionSet(patch.getOp(), activeConnectionPermissionPatch, permission); + + } + + // Create user permission if path has user prefix + else if (path.startsWith(USER_PERMISSION_PATCH_PATH_PREFIX)) { + + // Get identifier and type from patch operation + String identifier = path.substring(USER_PERMISSION_PATCH_PATH_PREFIX.length()); + ObjectPermission.Type type = ObjectPermission.Type.valueOf(patch.getValue()); + + // Create and update corresponding permission + ObjectPermission permission = new ObjectPermission(type, identifier); + updatePermissionSet(patch.getOp(), userPermissionPatch, permission); + + } + + // Create system permission if path is system path + else if (path.equals(SYSTEM_PERMISSION_PATCH_PATH)) { + + // Get identifier and type from patch operation + SystemPermission.Type type = SystemPermission.Type.valueOf(patch.getValue()); + + // Create and update corresponding permission + SystemPermission permission = new SystemPermission(type); + updatePermissionSet(patch.getOp(), systemPermissionPatch, permission); + + } + + // Otherwise, the path is not supported + else + throw new GuacamoleClientException("Unsupported patch path: \"" + path + "\""); + + } // end for each patch operation + + // Save the permission changes + connectionPermissionPatch.apply(user.getConnectionPermissions()); + connectionGroupPermissionPatch.apply(user.getConnectionGroupPermissions()); + activeConnectionPermissionPatch.apply(user.getActiveConnectionPermissions()); + userPermissionPatch.apply(user.getUserPermissions()); + systemPermissionPatch.apply(user.getSystemPermissions()); + + } + +} diff --git a/guacamole/src/main/webapp/app/rest/services/userService.js b/guacamole/src/main/webapp/app/rest/services/userService.js index bd124622b..3953f89f8 100644 --- a/guacamole/src/main/webapp/app/rest/services/userService.js +++ b/guacamole/src/main/webapp/app/rest/services/userService.js @@ -50,9 +50,10 @@ angular.module('rest').factory('userService', ['$injector', * If null, no filtering will be performed. Valid values are listed * within PermissionSet.ObjectType. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{User} objects - * upon success. + * @returns {Promise.>} + * A promise which will resolve with a map of @link{User} objects + * where each key is the identifier (username) of the corresponding + * user. */ service.getUsers = function getUsers(dataSource, permissionTypes) { diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js index fe15b1425..cc134fa94 100644 --- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js +++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js @@ -227,14 +227,14 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings PermissionSet.ObjectPermissionType.DELETE ]); - userPromise.then(function usersReceived(userArrays) { + userPromise.then(function usersReceived(allUsers) { var addedUsers = {}; $scope.manageableUsers = []; // For each user in each data source angular.forEach(dataSources, function addUserList(dataSource) { - angular.forEach(userArrays[dataSource], function addUser(user) { + angular.forEach(allUsers[dataSource], function addUser(user) { // Do not add the same user twice if (addedUsers[user.username])