From 1236a4ca1f676b307e763d539d1480d38dab7b5d Mon Sep 17 00:00:00 2001 From: Marco Fargetta Date: Mon, 26 Aug 2024 16:45:45 +0200 Subject: [PATCH] Add TPS ProfileMappingService to v2 APIs --- .../rest/base/ProfileMappingProcessor.java | 404 ++++++++++++++++++ .../tps/rest/v2/ProfileMappingServlet.java | 119 ++++++ .../rest/v2/filters/ProfileMappingACL.java | 31 ++ .../v2/filters/ProfileMappingAuthMethod.java | 21 + 4 files changed, 575 insertions(+) create mode 100644 base/tps/src/main/java/org/dogtagpki/server/tps/rest/base/ProfileMappingProcessor.java create mode 100644 base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/ProfileMappingServlet.java create mode 100644 base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingACL.java create mode 100644 base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingAuthMethod.java diff --git a/base/tps/src/main/java/org/dogtagpki/server/tps/rest/base/ProfileMappingProcessor.java b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/base/ProfileMappingProcessor.java new file mode 100644 index 00000000000..b4416a8030d --- /dev/null +++ b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/base/ProfileMappingProcessor.java @@ -0,0 +1,404 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.tps.rest.base; + +import java.security.Principal; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.dogtagpki.server.rest.v2.PKIServlet; +import org.dogtagpki.server.tps.TPSEngine; +import org.dogtagpki.server.tps.TPSSubsystem; +import org.dogtagpki.server.tps.config.ProfileMappingDatabase; +import org.dogtagpki.server.tps.config.ProfileMappingRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netscape.certsrv.base.BadRequestException; +import com.netscape.certsrv.base.ForbiddenException; +import com.netscape.certsrv.base.PKIException; +import com.netscape.certsrv.common.Constants; +import com.netscape.certsrv.logging.AuditEvent; +import com.netscape.certsrv.logging.ILogger; +import com.netscape.certsrv.tps.profile.ProfileMappingCollection; +import com.netscape.certsrv.tps.profile.ProfileMappingData; +import com.netscape.cmscore.apps.CMS; +import com.netscape.cmscore.logging.Auditor; + +/** + * @author Marco Fargetta {@literal } + * @author Endi S. Dewata + */ +public class ProfileMappingProcessor { + private static final Logger logger = LoggerFactory.getLogger(ProfileMappingProcessor.class); + + private TPSSubsystem subsystem; + private ProfileMappingDatabase database; + private Auditor auditor; + + public ProfileMappingProcessor(TPSEngine engine) { + subsystem = (TPSSubsystem) engine.getSubsystem(TPSSubsystem.ID); + database = subsystem.getProfileMappingDatabase(); + auditor = engine.getAuditor(); + } + + public ProfileMappingCollection findProfileMappings(String filter, int start, int size) { + logger.debug("ProfileMappingProcessor.findProfileMappings()"); + + if (filter != null && filter.length() < PKIServlet.MIN_FILTER_LENGTH) { + throw new BadRequestException("Filter is too short."); + } + try { + Iterator profileMappings = database.findRecords(filter).iterator(); + + ProfileMappingCollection response = new ProfileMappingCollection(); + int i = 0; + + // skip to the start of the page + for (; i < start && profileMappings.hasNext(); i++) + profileMappings.next(); + + // return entries up to the page size + for (; i < start + size && profileMappings.hasNext(); i++) { + response.addEntry(createProfileMappingData(profileMappings.next())); + } + + // count the total entries + for (; profileMappings.hasNext(); i++) + profileMappings.next(); + response.setTotal(i); + + return response; + + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + throw new PKIException(e); + } + } + + public ProfileMappingData addProfileMapping(Principal principal, ProfileMappingData profileMappingData) { + String method = "ProfileMappingProcessor.addProfileMapping"; + + logger.debug("ProfileMappingProcessor.addProfileMapping(\"{}\")", profileMappingData.getProfileMappingID()); + ProfileMappingData pmd = null; + + try { + String status = profileMappingData.getStatus(); + + if (StringUtils.isEmpty(status) || database.requiresApproval() && !database.canApprove(principal)) { + // if status is unspecified or user doesn't have rights to approve, the entry is disabled + profileMappingData.setStatus(Constants.CFG_DISABLED); + } + String id = profileMappingData.getProfileMappingID(); + database.addRecord(id, createProfileMappingRecord(profileMappingData)); + pmd = createProfileMappingData(database.getRecord(id)); + auditMappingResolverChange(principal, ILogger.SUCCESS, method, pmd.getID(), + profileMappingData.getProperties(), null); + + return pmd; + + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw new PKIException(e); + } + } + + public ProfileMappingData getProfileMapping(String profileMappingID) { + logger.debug("ProfileMappingProcessor.getProfileMapping(\"{}\")", profileMappingID); + + try { + return createProfileMappingData(database.getRecord(profileMappingID)); + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + throw new PKIException(e); + } + } + + public ProfileMappingData updateProfileMapping(Principal principal, String profileMappingID, ProfileMappingData profileMappingData) { + String method = "ProfileMappingProcessor.updateProfileMapping"; + + logger.debug("ProfileMappingProcessor.updateProfileMapping(\"{}\")", profileMappingID); + + try { + ProfileMappingRecord pmRecord = database.getRecord(profileMappingID); + // only disabled profile mapping can be updated + if (!Constants.CFG_DISABLED.equals(pmRecord.getStatus())) { + Exception e = new ForbiddenException("Unable to update profile mapping " + profileMappingID); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw e; + } + // update status if specified + String status = profileMappingData.getStatus(); + boolean statusChanged = false; + if (status != null && !Constants.CFG_DISABLED.equals(status)) { + if (!Constants.CFG_ENABLED.equals(status)) { + Exception e = new ForbiddenException("Invalid profile mapping status: " + status); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw e; + } + // if user doesn't have rights, set to pending + if (database.requiresApproval() && !database.canApprove(principal)) { + status = Constants.CFG_PENDING_APPROVAL; + } + // enable profile mapping + pmRecord.setStatus(status); + statusChanged = true; + } + // update properties if specified + Map properties = profileMappingData.getProperties(); + if (properties != null) { + pmRecord.setProperties(properties); + } + + database.updateRecord(profileMappingID, pmRecord); + + profileMappingData = createProfileMappingData(database.getRecord(profileMappingID)); + if (statusChanged) { + if (properties == null) { + properties = new HashMap<>(); + } + properties.put("Status", status); + } + auditMappingResolverChange(principal, ILogger.SUCCESS, method, profileMappingData.getID(), properties, null); + + return profileMappingData; + + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingData.getID(), + profileMappingData.getProperties(), e.toString()); + throw new PKIException(e); + } + } + + public ProfileMappingData changeStatus(Principal principal, String profileMappingID, String action) { + String method = "ProfileMappingProcessor.changeStatus"; + + Map auditModParams = new HashMap<>(); + + if (profileMappingID == null) { + auditConfigTokenGeneral(principal, ILogger.FAILURE, method, null, + "Profile mapper ID is null."); + throw new BadRequestException("Profile mapper ID is null."); + } + + if (action == null) { + auditConfigTokenGeneral(principal, ILogger.FAILURE, method, auditModParams, + "action is null."); + throw new BadRequestException("Action is null."); + } + auditModParams.put("Action", action); + + logger.debug("ProfileMappingProcessor.changeStatus(\"{}\", \"{}\")", profileMappingID, action); + try { + ProfileMappingRecord pmRecord = database.getRecord(profileMappingID); + boolean statusChanged = false; + String status = pmRecord.getStatus(); + + boolean canApprove = database.canApprove(principal); + + if (Constants.CFG_DISABLED.equals(status)) { + + if (database.requiresApproval()) { + + if ("submit".equals(action) && !canApprove) { + status = Constants.CFG_PENDING_APPROVAL; + statusChanged = true; + + } else if ("enable".equals(action) && canApprove) { + status = Constants.CFG_ENABLED; + statusChanged = true; + + } else { + Exception e = new BadRequestException("Invalid action: " + action); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + + } else { + if ("enable".equals(action)) { + status = Constants.CFG_ENABLED; + statusChanged = true; + + } else { + Exception e = new BadRequestException("Invalid action: " + action); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + } + + } else if (Constants.CFG_ENABLED.equals(status)) { + + if ("disable".equals(action)) { + status = Constants.CFG_DISABLED; + statusChanged = true; + + } else { + Exception e = new BadRequestException("Invalid action: " + action); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + + } else if (Constants.CFG_PENDING_APPROVAL.equals(status)) { + + if ("approve".equals(action) && canApprove) { + status = Constants.CFG_ENABLED; + statusChanged = true; + + } else if ("reject".equals(action) && canApprove) { + status = Constants.CFG_DISABLED; + statusChanged = true; + + } else if ("cancel".equals(action) && !canApprove) { + status = Constants.CFG_DISABLED; + statusChanged = true; + + } else { + Exception e = new BadRequestException("Invalid action: " + action); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + + } else { + Exception e = new PKIException("Invalid profile mapping status: " + status); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + + pmRecord.setStatus(status); + database.updateRecord(profileMappingID, pmRecord); + + ProfileMappingData profileMappingData = createProfileMappingData(database.getRecord(profileMappingID)); + + if (statusChanged) { + auditModParams.put("Status", status); + } + auditMappingResolverChange(principal, ILogger.SUCCESS, method, profileMappingData.getID(), auditModParams, null); + + return profileMappingData; + + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw new PKIException(e); + } + } + + public void removeProfileMapping(Principal principal, String profileMappingID) { + String method = "ProfileMappingProcessor.removeProfileMapping"; + Map auditModParams = new HashMap<>(); + + logger.debug("ProfileMappingProcessor.removeProfileMapping(\"{}\")", profileMappingID); + + try { + ProfileMappingRecord pmRecord = database.getRecord(profileMappingID); + String status = pmRecord.getStatus(); + + if (!Constants.CFG_DISABLED.equals(status)) { + Exception e = new ForbiddenException("Unable to delete profile mapping " + profileMappingID); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + } + database.removeRecord(profileMappingID); + auditMappingResolverChange(principal, ILogger.SUCCESS, method, profileMappingID, null, null); + } catch (PKIException e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw e; + + } catch (Exception e) { + logger.error("ProfileMappingProcessor: " + e.getMessage(), e); + auditMappingResolverChange(principal, ILogger.FAILURE, method, profileMappingID, + auditModParams, e.toString()); + throw new PKIException(e); + } + } + + private ProfileMappingData createProfileMappingData(ProfileMappingRecord profileMappingRecord) { + + ProfileMappingData profileMappingData = new ProfileMappingData(); + profileMappingData.setID(profileMappingRecord.getID()); + profileMappingData.setProfileMappingID(profileMappingRecord.getID()); + profileMappingData.setStatus(profileMappingRecord.getStatus()); + profileMappingData.setProperties(profileMappingRecord.getProperties()); + return profileMappingData; + } + + private ProfileMappingRecord createProfileMappingRecord(ProfileMappingData profileMappingData) { + + String id = profileMappingData.getID(); + ProfileMappingRecord profileMappingRecord = new ProfileMappingRecord(); + profileMappingRecord.setID(id == null ? profileMappingData.getProfileMappingID() : id); + profileMappingRecord.setStatus(profileMappingData.getStatus()); + profileMappingRecord.setProperties(profileMappingData.getProperties()); + return profileMappingRecord; + } + + private void auditMappingResolverChange(Principal principal, String status, String service, String resolverID, Map params, + String info) { + + String msg = CMS.getLogMessage( + AuditEvent.CONFIG_TOKEN_MAPPING_RESOLVER, + principal.getName(), + status, + service, + resolverID, + auditor.getParamString(params), + info); + auditor.log(msg); + } + + private void auditConfigTokenGeneral(Principal principal, String status, String service, Map params, String info) { + String msg = CMS.getLogMessage( + AuditEvent.CONFIG_TOKEN_GENERAL, + principal.getName(), + status, + service, + auditor.getParamString(params), + info); + auditor.log(msg); + } +} diff --git a/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/ProfileMappingServlet.java b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/ProfileMappingServlet.java new file mode 100644 index 00000000000..bb7a1030a75 --- /dev/null +++ b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/ProfileMappingServlet.java @@ -0,0 +1,119 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.tps.rest.v2; + +import java.io.PrintWriter; +import java.net.URLEncoder; +import java.util.stream.Collectors; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.dogtagpki.server.tps.rest.base.ProfileMappingProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.netscape.certsrv.base.WebAction; +import com.netscape.certsrv.tps.profile.ProfileMappingCollection; +import com.netscape.certsrv.tps.profile.ProfileMappingData; +import com.netscape.certsrv.util.JSONSerializer; + +/** + * @author Marco Fargetta {@literal } + */ +@WebServlet( + name = "tpsProfileMapping", + urlPatterns = "/v2/profile-mappings/*") +public class ProfileMappingServlet extends TPSServlet { + private static final long serialVersionUID = 1L; + private static final Logger logger = LoggerFactory.getLogger(ProfileMappingServlet.class); + + private ProfileMappingProcessor profileMappingProcessor; + + @Override + public void init() throws ServletException { + super.init(); + profileMappingProcessor = new ProfileMappingProcessor(getTPSEngine()); + } + + @WebAction(method = HttpMethod.GET, paths = {""}) + public void findProfileMappings(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.findProfileMappings(): session: {}", session.getId()); + int size = request.getParameter("pageSize") == null ? + DEFAULT_SIZE : Integer.parseInt(request.getParameter("pageSize")); + int start = request.getParameter("start") == null ? 0 : Integer.parseInt(request.getParameter("start")); + String filter = request.getParameter("filter"); + ProfileMappingCollection profileMappings = profileMappingProcessor.findProfileMappings(filter, start, size); + PrintWriter out = response.getWriter(); + out.println(profileMappings.toJSON()); + } + + @WebAction(method = HttpMethod.POST, paths = {""}) + public void addProfileMapping(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.addProfileMapping(): session: {}", session.getId()); + String requestData = request.getReader().lines().collect(Collectors.joining()); + ProfileMappingData profileMappingData = JSONSerializer.fromJSON(requestData, ProfileMappingData.class); + ProfileMappingData newProfileMapping = profileMappingProcessor.addProfileMapping(request.getUserPrincipal(), profileMappingData); + String encodedID = URLEncoder.encode(newProfileMapping.getID(), "UTF-8"); + StringBuffer uri = request.getRequestURL(); + uri.append("/" + encodedID); + response.setHeader("Location", uri.toString()); + response.setStatus(HttpServletResponse.SC_CREATED); + PrintWriter out = response.getWriter(); + out.println(newProfileMapping.toJSON()); + } + + @WebAction(method = HttpMethod.GET, paths = {"{}"}) + public void getProfileMapping(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.getProfileMapping(): session: {}", session.getId()); + String[] pathElement = request.getPathInfo().substring(1).split("/"); + String profileMappingID = pathElement[0]; + ProfileMappingData profileMapping = profileMappingProcessor.getProfileMapping(profileMappingID); + PrintWriter out = response.getWriter(); + out.println(profileMapping.toJSON()); + } + + @WebAction(method = HttpMethod.PATCH, paths = {"{}"}) + public void updateProfileMapping(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.updateProfileMapping(): session: {}", session.getId()); + String[] pathElement = request.getPathInfo().substring(1).split("/"); + String profileMappingID = pathElement[0]; + String requestData = request.getReader().lines().collect(Collectors.joining()); + ProfileMappingData profileMappingData = JSONSerializer.fromJSON(requestData, ProfileMappingData.class); + ProfileMappingData profileMapping = profileMappingProcessor.updateProfileMapping(request.getUserPrincipal(), profileMappingID, profileMappingData); + PrintWriter out = response.getWriter(); + out.println(profileMapping.toJSON()); + } + + @WebAction(method = HttpMethod.POST, paths = {"{}"}) + public void changeStatus(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.changeStatus(): session: {}", session.getId()); + String[] pathElement = request.getPathInfo().substring(1).split("/"); + String profileMappingID = pathElement[0]; + String action = request.getParameter("action"); + ProfileMappingData profileMapping = profileMappingProcessor.changeStatus(request.getUserPrincipal(), profileMappingID, action); + PrintWriter out = response.getWriter(); + out.println(profileMapping.toJSON()); + } + + @WebAction(method = HttpMethod.DELETE, paths = {"{}"}) + public void removeProfileMapping(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpSession session = request.getSession(); + logger.debug("ProfileMappingServlet.removeProfileMapping(): session: {}", session.getId()); + String[] pathElement = request.getPathInfo().substring(1).split("/"); + String profileMappingID = pathElement[0]; + profileMappingProcessor.removeProfileMapping(request.getUserPrincipal(), profileMappingID); + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + } +} diff --git a/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingACL.java b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingACL.java new file mode 100644 index 00000000000..ea8d842b3ab --- /dev/null +++ b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingACL.java @@ -0,0 +1,31 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.tps.rest.v2.filters; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebFilter; + +import org.dogtagpki.server.rest.v2.filters.ACLFilter; + +@WebFilter(servletNames = "tpsProfileMapping") +public class ProfileMappingACL extends ACLFilter { + private static final long serialVersionUID = 1L; + + @Override + public void init() throws ServletException { + setAcl("profile-mappings.read"); + + Map aclMap = new HashMap<>(); + aclMap.put("POST:", "profile-mappings.add"); + aclMap.put("PATCH:{}", "profile-mappings.modify"); + aclMap.put("POST:{}", "profiles-mappings.change-status"); + aclMap.put("DELETE:{}", "profile-mappings.remove"); + setAclMap(aclMap); + } +} diff --git a/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingAuthMethod.java b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingAuthMethod.java new file mode 100644 index 00000000000..b377579dbd5 --- /dev/null +++ b/base/tps/src/main/java/org/dogtagpki/server/tps/rest/v2/filters/ProfileMappingAuthMethod.java @@ -0,0 +1,21 @@ +// +// Copyright Red Hat, Inc. +// +// SPDX-License-Identifier: GPL-2.0-or-later +// +package org.dogtagpki.server.tps.rest.v2.filters; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebFilter; + +import org.dogtagpki.server.rest.v2.filters.AuthMethodFilter; + +@WebFilter(servletNames = "tpsProfileMapping") +public class ProfileMappingAuthMethod extends AuthMethodFilter { + private static final long serialVersionUID = 1L; + + @Override + public void init() throws ServletException { + setAuthMethod("profile-mappings"); + } +}