diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java index d7ea84bbb..35d8e9aea 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTModule.java @@ -30,6 +30,7 @@ import org.glyptodon.guacamole.net.basic.rest.auth.TokenUserContextMap; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupService; import org.glyptodon.guacamole.net.basic.rest.permission.PermissionService; +import org.glyptodon.guacamole.net.basic.rest.user.UserService; import org.glyptodon.guacamole.properties.GuacamoleProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,6 +70,7 @@ public class RESTModule extends AbstractModule { bind(ConnectionService.class); bind(ConnectionGroupService.class); bind(PermissionService.class); + bind(UserService.class); bind(AuthenticationService.class); bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index a2f960dfc..c12a8b144 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -26,6 +26,7 @@ import org.glyptodon.guacamole.net.basic.rest.auth.LoginRESTService; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; import org.glyptodon.guacamole.net.basic.rest.permission.PermissionRESTService; +import org.glyptodon.guacamole.net.basic.rest.user.UserRESTService; /** * A Guice Module to set up the servlet mappings for the Guacamole REST API. @@ -41,6 +42,7 @@ public class RESTServletModule extends ServletModule { bind(ConnectionRESTService.class); bind(ConnectionGroupRESTService.class); bind(PermissionRESTService.class); + bind(UserRESTService.class); bind(LoginRESTService.class); // Set up the servlet and JSON mappings diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java index f73f05177..1c9043c4b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionRESTService.java @@ -19,9 +19,7 @@ package org.glyptodon.guacamole.net.basic.rest.permission; */ import com.google.inject.Inject; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -36,7 +34,6 @@ import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.User; import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.basic.rest.HTTPException; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java index 3b9626bbf..32314b84d 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/permission/PermissionService.java @@ -39,8 +39,7 @@ public class PermissionService { * @param permissions The Connections to convert for REST endpoint use. * @return A List of APIPermission objects for use with the REST endpoint. */ - public List convertPermissionList(Iterable permissions) - throws GuacamoleException { + public List convertPermissionList(Iterable permissions) { List restPermissions = new ArrayList(); for(Permission permission : permissions) { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java new file mode 100644 index 000000000..66b9ec3d5 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUser.java @@ -0,0 +1,82 @@ +package org.glyptodon.guacamole.net.basic.rest.user; + +import org.codehaus.jackson.map.annotate.JsonSerialize; +import org.glyptodon.guacamole.net.auth.User; + +/* + * Guacamole - Clientless Remote Desktop + * Copyright (C) 2010 Michael Jumper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * A simple User to expose through the REST endpoints. + * + * @author James Muehlner + */ +@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) +public class APIUser { + + /** + * The username of this user. + */ + private String username; + + /** + * The password of this user. + */ + private String password; + + /** + * Construct a new APIUser from the provided User. + * @param user The User to construct the APIUser from. + */ + public APIUser(User user) { + this.username = user.getUsername(); + this.password = user.getPassword(); + } + + /** + * Returns the username for this user. + * @return The username for this user. + */ + public String getUsername() { + return username; + } + + /** + * Set the username for this user. + * @param username The username for this user. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns the password for this user. + * @return The password for this user. + */ + public String getPassword() { + return password; + } + + /** + * Set the password for this user. + * @param password The password for this user. + */ + public void setPassword(String password) { + this.password = password; + } +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java new file mode 100644 index 000000000..db347457c --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/APIUserWrapper.java @@ -0,0 +1,103 @@ +package org.glyptodon.guacamole.net.basic.rest.user; + +/* + * Guacamole - Clientless Remote Desktop + * Copyright (C) 2010 Michael Jumper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.Collections; +import java.util.Set; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.User; +import org.glyptodon.guacamole.net.auth.permission.Permission; + +/** + * A wrapper to make an APIConnection look like a User. Useful where a + * org.glyptodon.guacamole.net.auth.User is required. + * + * @author James Muehlner + */ +public class APIUserWrapper implements User { + + /** + * The wrapped APIUser. + */ + private APIUser apiUser; + + /** + * The set of permissions for this user. + * NOTE: Not exposed by the REST endpoints. + */ + private Set permissionSet = Collections.EMPTY_SET; + + /** + * Wrap a given APIUser to expose as a User. + * @param apiUser The APIUser to wrap. + */ + public APIUserWrapper(APIUser apiUser) { + this.apiUser = apiUser; + } + + /** + * Wrap a given APIUser to expose as a User, with the given permission set. + * @param apiUser The APIUser to wrap. + * @param permissionSet The set of permissions for the wrapped user. + */ + public APIUserWrapper(APIUser apiUser, Set permissionSet) { + this.apiUser = apiUser; + this.permissionSet = permissionSet; + } + + @Override + public String getUsername() { + return apiUser.getUsername(); + } + + @Override + public void setUsername(String username) { + apiUser.setUsername(username); + } + + @Override + public String getPassword() { + return apiUser.getPassword(); + } + + @Override + public void setPassword(String password) { + apiUser.setPassword(password); + } + + @Override + public Set getPermissions() throws GuacamoleException { + return permissionSet; + } + + @Override + public boolean hasPermission(Permission permission) throws GuacamoleException { + return permissionSet.contains(permission); + } + + @Override + public void addPermission(Permission permission) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } + + @Override + public void removePermission(Permission permission) throws GuacamoleException { + throw new UnsupportedOperationException("Operation not supported."); + } +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java new file mode 100644 index 000000000..44c5c603f --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -0,0 +1,229 @@ +package org.glyptodon.guacamole.net.basic.rest.user; + +import com.google.inject.Inject; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +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.MediaType; +import javax.ws.rs.core.Response; +import org.glyptodon.guacamole.GuacamoleClientException; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleSecurityException; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.User; +import org.glyptodon.guacamole.net.auth.UserContext; +import org.glyptodon.guacamole.net.basic.rest.HTTPException; +import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/* + * Guacamole - Clientless Remote Desktop + * Copyright (C) 2010 Michael Jumper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + + +/** + * A REST Service for handling user CRUD operations. + * + * @author James Muehlner + */ +@Path("/api/user") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class UserRESTService { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(UserRESTService.class); + + /** + * A service for authenticating users from auth tokens. + */ + @Inject + private AuthenticationService authenticationService; + + /** + * A service for managing the REST endpoint APIPermission objects. + */ + @Inject + private UserService userService; + + /** + * Get a list of users in the system. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @return The user list. + */ + @GET + public List getUsers(@QueryParam("token") String authToken) { + UserContext userContext = authenticationService.getUserContextFromAuthToken(authToken); + + try { + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Convert and return the user directory listing + return userService.convertUserList(userDirectory); + } catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.UNAUTHORIZED, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while listing permissions.", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + } + + /** + * Get a user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @return user The user. + */ + @GET + @Path("/{userID}") + public APIUser getUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) { + UserContext userContext = authenticationService.getUserContextFromAuthToken(authToken); + + try { + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user + User user = userDirectory.get(userID); + + if(user == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + // Return the user + return new APIUser(user); + } catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.UNAUTHORIZED, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while listing permissions.", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + } + + /** + * Create a new user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param user The new user to create. + */ + @POST + public void createUser(@QueryParam("token") String authToken, APIUser user) { + UserContext userContext = authenticationService.getUserContextFromAuthToken(authToken); + + try { + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Create the user + userDirectory.add(new APIUserWrapper(user)); + } catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.UNAUTHORIZED, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while listing permissions.", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + } + + /** + * Update an existing user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The unique identifier of the user to update. + * @param user The updated user. + */ + @POST + @Path("/{userID}") + public void updateUser(@QueryParam("token") String authToken, @PathParam("userID") String userID, APIUser user) { + UserContext userContext = authenticationService.getUserContextFromAuthToken(authToken); + + try { + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + if(!user.getUsername().equals(userID)) + throw new HTTPException(Response.Status.BAD_REQUEST, "Username does not match provided userID."); + + // Get the user + User existingUser = userDirectory.get(userID); + + if(existingUser == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + /* + * Update the user with the permission set from the existing user + * since the user REST endpoints do not expose permissions + */ + userDirectory.update(new APIUserWrapper(user, existingUser.getPermissions())); + } catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.UNAUTHORIZED, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while listing permissions.", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + } + + /** + * Delete an existing user. + * @param authToken The authentication token that is used to authenticate + * the user performing the operation. + * @param userID The unique identifier of the user to delete. + */ + @DELETE + @Path("/{userID}") + public void deleteUser(@QueryParam("token") String authToken, @PathParam("userID") String userID) { + UserContext userContext = authenticationService.getUserContextFromAuthToken(authToken); + + try { + // Get the directory + Directory userDirectory = userContext.getUserDirectory(); + + // Get the user + User existingUser = userDirectory.get(userID); + + if(existingUser == null) + throw new HTTPException(Response.Status.NOT_FOUND, "User not found with the provided userID."); + + // Delete the user + userDirectory.remove(userID); + } catch(GuacamoleSecurityException e) { + throw new HTTPException(Response.Status.UNAUTHORIZED, e.getMessage() != null ? e.getMessage() : "Permission denied."); + } catch(GuacamoleClientException e) { + throw new HTTPException(Response.Status.BAD_REQUEST, e.getMessage() != null ? e.getMessage() : "Invalid Request."); + } catch(GuacamoleException e) { + logger.error("Unexpected GuacamoleException caught while listing permissions.", e); + throw new HTTPException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage() != null ? e.getMessage() : "Unexpected server error."); + } + } +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java new file mode 100644 index 000000000..5c6de105f --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserService.java @@ -0,0 +1,54 @@ +package org.glyptodon.guacamole.net.basic.rest.user; + +import java.util.ArrayList; +import java.util.List; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.auth.Directory; +import org.glyptodon.guacamole.net.auth.User; + +/* + * Guacamole - Clientless Remote Desktop + * Copyright (C) 2010 Michael Jumper + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * A service for performing useful manipulations on REST Users. + * + * @author James Muehlner + */ +public class UserService { + + /** + * Converts a user directory to a list of APIUser objects for + * exposing with the REST endpoints. + * + * @param userDirectory The user directory to convert for REST endpoint use. + * @return A List of APIUser objects for use with the REST endpoint. + * @throws GuacamoleException If an error occurs while converting the + * user directory. + */ + public List convertUserList(Directory userDirectory) + throws GuacamoleException { + List restUsers = new ArrayList(); + + for(String username : userDirectory.getIdentifiers()) { + restUsers.add(new APIUser(userDirectory.get(username))); + } + + return restUsers; + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java new file mode 100644 index 000000000..aa411d037 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/package-info.java @@ -0,0 +1,6 @@ + +/** + * Classes related to the user manipulation aspect of the Guacamole REST API. + */ +package org.glyptodon.guacamole.net.basic.rest.user; +