More RESTful API

Jens Pelzetter 2020-05-26 15:18:17 +02:00
parent ca2e2c3f97
commit 81232cd19a
7 changed files with 578 additions and 28 deletions

View File

@ -92,6 +92,12 @@
<artifactId>flyway-core</artifactId> <artifactId>flyway-core</artifactId>
</dependency> </dependency>
<!-- OpenAPI/Swagger Annoations for documenting the RESTful API
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>-->
<!-- Dependencies for log4j 2 --> <!-- Dependencies for log4j 2 -->
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>

View File

@ -3,11 +3,10 @@
* To change this template file, choose Tools | Templates * To change this template file, choose Tools | Templates
* and open the template in the editor. * and open the template in the editor.
*/ */
package org.libreccm.core.api; package org.libreccm.api;
import org.libreccm.api.CorsFilter; import org.libreccm.security.GroupsApi;
import org.libreccm.api.DefaultResponseHeaders; import org.libreccm.security.RolesApi;
import org.libreccm.api.PreflightRequestFilter;
import org.libreccm.security.UsersApi; import org.libreccm.security.UsersApi;
import java.util.HashSet; import java.util.HashSet;
@ -20,17 +19,22 @@ import javax.ws.rs.core.Application;
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@ApplicationPath("/api") @ApplicationPath("/api/admin")
public class CcmCoreApi extends Application { public class AdminApi extends Application {
@Override @Override
public Set<Class<?>> getClasses() { public Set<Class<?>> getClasses() {
final Set<Class<?>> classes = new HashSet<>(); final Set<Class<?>> classes = new HashSet<>();
classes.add(CorsFilter.class); classes.add(CorsFilter.class);
classes.add(DefaultResponseHeaders.class); classes.add(DefaultResponseHeaders.class);
classes.add(PreflightRequestFilter.class); classes.add(PreflightRequestFilter.class);
classes.add(GroupsApi.class);
classes.add(RolesApi.class);
classes.add(UsersApi.class); classes.add(UsersApi.class);
return classes; return classes;
} }

View File

@ -0,0 +1,466 @@
/*
* Copyright (C) 2020 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/package org.libreccm.security;
import org.libreccm.core.CoreConstants;
import org.libreccm.core.api.ExtractedIdentifier;
import org.libreccm.core.api.IdentifierExtractor;
import org.libreccm.core.api.JsonArrayCollector;
import java.net.URI;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
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.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path("/groups")
public class GroupsApi {
@Inject
private IdentifierExtractor identifierExtractor;
@Inject
private GroupManager groupManager;
@Inject
private GroupRepository groupRepository;
@Inject
private RoleManager roleManager;
@Inject
private RoleRepository roleRepository;
@Inject
private UserManager userManager;
@Inject
private UserRepository userRepository;
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public JsonArray getGroups(
@QueryParam("limit") @DefaultValue("20") final int limit,
@QueryParam("offset") @DefaultValue("0") final int offset
) {
return groupRepository
.findAll(limit, offset)
.stream()
.map(Group::buildJson)
.map(JsonObjectBuilder::build)
.collect(new JsonArrayCollector());
}
@GET
@Path("/{groupIdentifier}")
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public JsonObject getGroup(
@PathParam("groupIdentifier") final String identifierParam
) {
return findGroup(identifierParam).toJson();
}
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response addGroup(final JsonObject groupData) {
if (groupData.isNull("name")
|| groupData.getString("name").matches("\\s*")) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity("Name of new group is missing.")
.build();
}
final String name = groupData.getString("name");
if (groupRepository.findByName(name).isPresent()) {
return Response
.status(Response.Status.CONFLICT)
.entity(
String.format(
"A group with the name %s already exists.",
name
)
)
.build();
}
final Group group = new Group();
group.setName(name);
groupRepository.save(group);
return Response
.status(Response.Status.CREATED)
.contentLocation(
URI.create(String.format("/api/groups/%s", group.getName()))
)
.build();
}
@PUT
@Path("/{groupIdentifier}")
@Consumes(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response updateGroup(
@PathParam("groupIdentifier") final String groupIdentifier,
final JsonObject groupData
) {
final Group group = findGroup(groupIdentifier);
boolean updated = false;
if (!groupData.isNull("name")
&& !groupData.getString("name").matches("\\s*")
&& !groupData.getString("name").equals(group.getName())) {
group.setName(groupData.getString("name"));
updated = true;
}
if (updated) {
groupRepository.save(group);
}
return Response
.status(Response.Status.OK)
.entity(
String.format(
"Group %s updated successfully.", group.getName()
)
)
.build();
}
@DELETE
@Path("/{groupIdentifier}")
@Consumes(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response deleteGroup(
@PathParam("groupIdentifier") final String groupIdentifier
) {
final Group group = findGroup(groupIdentifier);
final String name = group.getName();
groupRepository.delete(group);
return Response
.ok(String.format("Group %s deleted successfully.", name))
.build();
}
@GET
@Path("/{groupIdentifier}/members")
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public JsonArray getMembers(
@PathParam("groupIdentifier") final String groupIdentifier
) {
return findGroup(groupIdentifier)
.getMemberships()
.stream()
.map(GroupMembership::toJson)
.collect(new JsonArrayCollector());
}
@PUT
@Path("/{groupIdentifier}/members/{userIdentifier}")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response addMember(
@PathParam("groupIdentifier") final String groupIdentifier,
@PathParam("userIdentifier") final String userIdentifier
) {
final Group group = findGroup(groupIdentifier);
final User user = findUser(userIdentifier);
groupManager.addMemberToGroup(user, group);
return Response
.ok(
String.format(
"User %s successfully added to group %s.",
user.getName(),
group.getName()
)
).build();
}
@DELETE
@Path("/{groupIdentifier}/members/{userIdentifier}")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response removeMember(
@PathParam("groupIdentifier") final String groupIdentifier,
@PathParam("userIdentifier") final String userIdentifier
) {
final Group group = findGroup(groupIdentifier);
final User user = findUser(userIdentifier);
groupManager.removeMemberFromGroup(user, group);
return Response
.ok()
.entity(
String.format(
"User %s successfully removed to group %s.",
user.getName(),
group.getName()
)
)
.build();
}
@GET
@Path("/{groupIdentifier}/roles")
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public JsonArray getRoleMemberships(
@PathParam("groupIdentifier")
final String groupIdentifier
) {
return findGroup(groupIdentifier)
.getRoleMemberships()
.stream()
.map(RoleMembership::toJson)
.collect(new JsonArrayCollector());
}
@PUT
@Path("/{groupIdentifier}/groups/{roleIdentifier}")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response addRoleMembership(
@PathParam("groupIdentifier")
final String groupIdentifier,
@PathParam("roleIdentifier")
final String roleIdentifier
) {
final Group group = findGroup(groupIdentifier);
final Role role = findRole(roleIdentifier);
roleManager.assignRoleToParty(role, group);
return Response
.ok()
.entity(
String.format(
"Role %s successfully assigned to group %s.",
role.getName(),
group.getName()
)
)
.build();
}
@DELETE
@Path("/{groupIdentifier}/groups/{roleIdentifier}")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public Response removeRoleMembership(
@PathParam("groupIdentifier")
final String groupIdentifier,
@PathParam("roleIdentifier")
final String roleIdentifier
) {
final Group group = findGroup(groupIdentifier);
final Role role = findRole(roleIdentifier);
roleManager.removeRoleFromParty(role, group);
return Response
.ok()
.entity(
String.format(
"Role %s successfully removed from group %s.",
role.getName(),
group.getName()
)
)
.build();
}
private Group findGroup(final String groupIdentifier) {
final ExtractedIdentifier identifier = identifierExtractor
.extractIdentifier(groupIdentifier);
switch (identifier.getType()) {
case ID:
return groupRepository
.findById(Long.parseLong(identifier.getIdentifier()))
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No group with ID %s found",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
case UUID:
return groupRepository
.findByUuid(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No group with UUID %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
default:
return groupRepository
.findByName(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No group with name %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
}
}
private Role findRole(final String roleIdentifier) {
final ExtractedIdentifier identifier = identifierExtractor
.extractIdentifier(roleIdentifier);
switch (identifier.getType()) {
case ID:
return roleRepository
.findById(Long.parseLong(identifier.getIdentifier()))
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No role with ID %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
case UUID:
return roleRepository
.findByUuid(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No role with UUID %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
default:
return roleRepository
.findByName(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No role with name %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
}
}
private User findUser(final String identifierParam) {
final ExtractedIdentifier identifier = identifierExtractor
.extractIdentifier(identifierParam);
switch (identifier.getType()) {
case ID:
return userRepository
.findById(Long.parseLong(identifier.getIdentifier()))
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No user with ID %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND)
);
case UUID:
return userRepository
.findByUuid(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No user with UUID %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND)
);
default:
return userRepository
.findByName(identifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No user with name %s found.",
identifier.getIdentifier()
),
Response.Status.NOT_FOUND)
);
}
}
}

View File

@ -22,7 +22,6 @@ import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.ObjectIdGenerators; import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.Field;
import org.hibernate.validator.constraints.NotBlank;
import org.libreccm.l10n.LocalizedString; import org.libreccm.l10n.LocalizedString;
import org.libreccm.workflow.TaskAssignment; import org.libreccm.workflow.TaskAssignment;
@ -57,6 +56,7 @@ import javax.persistence.NamedQuery;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
import javax.persistence.OrderBy; import javax.persistence.OrderBy;
import javax.persistence.Table; import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;

View File

@ -0,0 +1,19 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.libreccm.security;
import javax.enterprise.context.RequestScoped;
import javax.ws.rs.Path;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path("/roles")
public class RolesApi {
}

View File

@ -46,6 +46,8 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
/** /**
* Provides RESTful API endpoints for managing users. Access to all endpoints
* defined by this class requires admin privileges.
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@ -74,6 +76,14 @@ public class UsersApi {
@Inject @Inject
private UserRepository userRepository; private UserRepository userRepository;
/**
* Retrieves all users.
*
* @param limit How many users should be retrieved?
* @param offset The first user to retrieve
*
* @return A JSON array with the all users.
*/
@GET @GET
@Path("/") @Path("/")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -92,6 +102,13 @@ public class UsersApi {
.collect(new JsonArrayCollector()); .collect(new JsonArrayCollector());
} }
/**
* Retrieves a specific user.
*
* @param identifierParam The identifier for the user.
*
* @return A JSON representation of the user.
*/
@GET @GET
@Path("/{userIdentifier}") @Path("/{userIdentifier}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -104,6 +121,19 @@ public class UsersApi {
return findUser(identifierParam).toJson(); return findUser(identifierParam).toJson();
} }
/**
* Creates a new user.
*
* @param userData The data from which the user is generated.
*
* @return A HTTP response indicating the success or failure of the user
* generation. A 201 (Created) response code is send if the user was
* created successfully together with the URL of the new user. If
* the provided data does not contain a required value a response
* with the 400 (Bad Request) status code is returned. If a user
* with the name provided by the user data already exists a response
* with a 409 (Conflict) response is returned.
*/
@POST @POST
@Path("/") @Path("/")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@ -111,7 +141,6 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public Response addUser(final JsonObject userData) { public Response addUser(final JsonObject userData) {
final String givenName; final String givenName;
if (userData.isNull("givenName")) { if (userData.isNull("givenName")) {
givenName = null; givenName = null;
@ -188,6 +217,16 @@ public class UsersApi {
.build(); .build();
} }
/**
* Updates a user.
*
* @param userIdentifier The identifier of the user to update.
* @param userData The updated of the user. Please note that the name
* of a user can't be updated. If the user data
* contains a value for this property it is ignored.
*
* @return A 200 (OK) response if the user is succesfully updated.
*/
@PUT @PUT
@Path("/{userIdentifier}") @Path("/{userIdentifier}")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@ -195,8 +234,7 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public Response updateUser( public Response updateUser(
@PathParam("userIdentifier") @PathParam("userIdentifier") final String userIdentifier,
final String userIdentifier,
final JsonObject userData final JsonObject userData
) { ) {
final User user = findUser(userIdentifier); final User user = findUser(userIdentifier);
@ -236,6 +274,12 @@ public class UsersApi {
.build(); .build();
} }
/**
* Deletes a user.
*
* @param userIdentifier The identifier of the user to delete.
* @return A 200 (OK) response if the user was deleted successfully.
*/
@DELETE @DELETE
@Path("/{userIdentifier}") @Path("/{userIdentifier}")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@ -243,8 +287,7 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public Response deleteUser( public Response deleteUser(
@PathParam("userIdentifier") @PathParam("userIdentifier") final String userIdentifier
final String userIdentifier
) { ) {
final User user = findUser(userIdentifier); final User user = findUser(userIdentifier);
final String name = user.getName(); final String name = user.getName();
@ -262,8 +305,7 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public JsonArray getGroupMemberships( public JsonArray getGroupMemberships(
@PathParam("userIdentifier") @PathParam("userIdentifier") final String userIdentifier
final String userIdentifier
) { ) {
return findUser(userIdentifier) return findUser(userIdentifier)
.getGroupMemberships() .getGroupMemberships()
@ -278,10 +320,8 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public Response addGroupMembership( public Response addGroupMembership(
@PathParam("userIdentifier") @PathParam("userIdentifier") final String userIdentifier,
final String userIdentifier, @PathParam("groupIdentifier") final String groupIdentifier
@PathParam("groupIdentifier")
final String groupIdentifier
) { ) {
final User user = findUser(userIdentifier); final User user = findUser(userIdentifier);
final Group group = findGroup(groupIdentifier); final Group group = findGroup(groupIdentifier);
@ -306,10 +346,8 @@ public class UsersApi {
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public Response removeGroupMembership( public Response removeGroupMembership(
@PathParam("userIdentifier") @PathParam("userIdentifier") final String userIdentifier,
final String userIdentifier, @PathParam("groupIdentifier") final String groupIdentifier
@PathParam("groupIdentifier")
final String groupIdentifier
) { ) {
final User user = findUser(userIdentifier); final User user = findUser(userIdentifier);
final Group group = findGroup(groupIdentifier); final Group group = findGroup(groupIdentifier);
@ -320,7 +358,7 @@ public class UsersApi {
.ok() .ok()
.entity( .entity(
String.format( String.format(
"User %s successfully removed to group %s.", "User %s successfully removed from group %s.",
user.getName(), user.getName(),
group.getName() group.getName()
) )

17
pom.xml
View File

@ -721,6 +721,23 @@
<version>4.4.1</version> <version>4.4.1</version>
</dependency> </dependency>
<!-- OpenAPI Spec Generation -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-jaxrs2-servlet-initializer-v2</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.1.2</version>
</dependency>
<!-- <!--
********************** **********************
Dependencies for tests Dependencies for tests