DomainRestHandler.java
/*
** Module : DomainRestHandler.java
** Abstract : Class that handles requests for REST administration of domain authentication meta table.
**
** Copyright (c) 2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
** 001 OM 20250310 Initial implementation. Covers CRUD primitives.
*/
/*
** 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 may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.admin.server;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import org.eclipse.jetty.server.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.util.logging.*;
/**
* Implements {@code BaseAdminRestHandler} to handle CRUD operations on {@code _sec-authentication-domain}
* meta table. The authentication is performed by the parent {@code AdminRestHandler}.
*/
public class DomainRestHandler
extends BaseAdminRestHandler
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(DomainRestHandler.class);
/** The singleton which handles tenant REST requests. */
private static DomainRestHandler instance;
/**
* Constructs a new handler for administration of {@code _sec-authentication-domain} resource using REST
* APIs.
*
* @param ah
* The WebAuthHandler used for authorisation.
*/
protected DomainRestHandler(WebAuthHandler ah)
{
super("/admin/security/domains", ah);
}
/**
* Tries to initialize this REST handler. It does this by reading the directory and creating the
* {@code DomainRestHandler} according to the settings.
*
* @param ah
* The authentication handler which will be used for authorisation.
*
* @return An instance of {@link BaseAdminRestHandler} on success or {@code null} if the directory is not
* configured with a REST handler for tenant administration.
*/
public static DomainRestHandler initialize(WebAuthHandler ah)
{
if (instance == null)
{
instance = new DomainRestHandler(ah);
}
return instance;
}
/**
* Implement this method for handling the GET requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handleGet(String target, String body, HttpServletResponse response, String token)
{
ArrayList<String> errorSink = new ArrayList<>();
String[] tok = target.split("/");
if (tok.length == 1)
{
// /admin/system/domains/{ldb} - GET the list of all domains on a logical database
List<String>[] domains = new ArrayList[1];
wsm.executeInContext(token, () ->
{
AdminServerImpl.setTargetLive(true, false, true);
domains[0] = AdminServerImpl.listDomains(tok[0], errorSink);
}, true);
try
{
response.getWriter().write(printDomains(domains[0], errorSink));
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
}
else if (tok.length == 2)
{
// /admin/system/domains/{ldb}/{domain} - GET details of a domain on a logical database
HashMap<String, BaseDataType>[] domain = new HashMap[1];
String domainName = dropQuotes(tok[1]);
wsm.executeInContext(token, () ->
{
AdminServerImpl.setTargetLive(true, false, true);
domain[0] = AdminServerImpl.getDomain(tok[0], domainName, errorSink);
}, true);
try
{
response.getWriter().write(printDomain(domain[0], domainName, errorSink));
}
catch (IOException e)
{
LOG.log(Level.WARNING, "Could not write response due to " + e.getMessage());
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
}
else
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
response.setStatus(HttpServletResponse.SC_OK);
}
/**
* Implement this method for handling the POST requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handlePost(String target, String body, HttpServletResponse response, String token)
{
String[] tok = target.split("/");
if (tok.length != 1)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// /admin/system/domains
JsonNode[] rootNode = new JsonNode[1];
try
{
rootNode[0] = mapper.get().readTree(body);
}
catch (JsonProcessingException e)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
ArrayList<String> errorSink = new ArrayList<>();
Boolean[] success = new Boolean[1];
Runnable task = () ->
{
AdminServerImpl.setTargetLive(true, false, true);
success[0] = AdminServerImpl.addDomain(target, rootNode[0], errorSink);
};
boolean ok = executeAndWriteResponse(token, response, task, success, errorSink);
response.setStatus(ok ? HttpServletResponse.SC_OK : HttpServletResponse.SC_BAD_REQUEST);
}
/**
* Implement this method for handling the PUT requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handlePut(String target, String body, HttpServletResponse response, String token)
{
// /admin/system/domains/{ldbName}/{domain}
String[] toks = target.split("/");
if (toks.length != 2)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
JsonNode[] rootNode = new JsonNode[1];
try
{
rootNode[0] = mapper.get().readTree(body);
}
catch (JsonProcessingException e)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
ArrayList<String> errorSink = new ArrayList<>();
Boolean[] success = new Boolean[1];
Runnable task = () ->
{
AdminServerImpl.setTargetLive(true, false, true);
String domainName = dropQuotes(toks[1]);
success[0] = AdminServerImpl.updateDomain(toks[0], domainName, rootNode[0], errorSink);
};
boolean ok = executeAndWriteResponse(token, response, task, success, errorSink);
response.setStatus(ok ? HttpServletResponse.SC_OK : HttpServletResponse.SC_BAD_REQUEST);
}
/**
* Implement this method for handling the DELETE requests. The result should be written directly to
* {@code response}'s writer.
*
* @param target
* The relative target of the request. From the absolute URL, the context prefix was eliminated.
* @param body
* The payload of the request (optional).
* @param response
* The response as the {@link Response} object or a wrapper of that request.
* @param token
* The authentication token.
*/
@Override
protected void handleDelete(String target, String body, HttpServletResponse response, String token)
{
// admin/system/domains/{ldbname}/{domain-name}
String[] toks = target.split("/");
if (toks.length != 2)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
ArrayList<String> errorSink = new ArrayList<>();
Boolean[] success = new Boolean[1];
Runnable task = () ->
{
AdminServerImpl.setTargetLive(true, false, true);
String domainName = dropQuotes(toks[1]);
success[0] = AdminServerImpl.deleteDomain(toks[0], domainName, errorSink);
};
boolean ok = executeAndWriteResponse(token, response, task, success, errorSink);
response.setStatus(ok ? HttpServletResponse.SC_OK : HttpServletResponse.SC_BAD_REQUEST);
}
/**
* Unquotes a name. In case of domains, the name can be empty, which poses a problem when passing in
* API target. The solution is to quote it. This method will restore its original value.
*
* @param name
* The name (of a domain) to be unquoted.
*
* @return The unquoted name.
*/
private static String dropQuotes(String name)
{
if (name == null || name.length() < 2)
{
return name;
}
if (name.charAt(0) == '\"' && name.charAt(name.length() - 1) == '\"')
{
return name.substring(1, name.length() - 1);
}
if (name.charAt(0) == '\'' && name.charAt(name.length() - 1) == '\'')
{
return name.substring(1, name.length() - 1);
}
return name;
}
/**
* List all the domains from the given list in a JSON format.
*
* @param domains
* The list with the domains.
* @param errorSink
* A sink for errors encountered.
*
* @return A string in JSON format with the domain information.
*/
private String printDomains(List<String> domains, List<String> errorSink)
{
if ((domains == null || domains.isEmpty()) && errorSink.isEmpty())
{
return "{\n\"domains\":[]\n}";
}
ObjectMapper objectMapper = mapper.get();
ObjectNode outerNode = objectMapper.createObjectNode();
if (domains != null && !domains.isEmpty())
{
ArrayNode domainArray = objectMapper.createArrayNode();
for (String d : domains)
{
domainArray.add(d);
}
outerNode.set("domains", domainArray);
}
if (!errorSink.isEmpty())
{
ArrayNode errorArray = objectMapper.createArrayNode();
for (String errMsg : errorSink)
{
errorArray.add(errMsg);
}
outerNode.set("errors", errorArray);
}
return outerNode.toString();
}
/**
* List all the attributes of a domain in a JSON format.
*
* @param domain
* The list with the domains.
* @param domainName
* The requested domain name.
* @param errorSink
* A sink for errors encountered.
*
* @return A string in JSON format with the domain information.
*/
private String printDomain(HashMap<String, BaseDataType> domain, String domainName, List<String> errorSink)
{
ObjectMapper objectMapper = mapper.get();
ObjectNode outerNode = objectMapper.createObjectNode();
if (domain != null)
{
ObjectNode domainNode = objectMapper.createObjectNode();
for (Map.Entry<String, BaseDataType> prop : domain.entrySet())
{
BaseDataType val = prop.getValue();
if (val.isUnknown())
{
domainNode.put(prop.getKey(), (String) null);
continue;
}
switch (val.getTypeName())
{
case "LOGICAL":
domainNode.put(prop.getKey(), ((logical) val).toJavaType());
break;
case "DECIMAL":
domainNode.put(prop.getKey(), ((decimal) val).toBigDecimal());
break;
case "INT":
case "INT64":
domainNode.put(prop.getKey(), ((int64) val).longValue());
break;
default:
domainNode.put(prop.getKey(), val.toStringMessage());
break;
}
}
outerNode.set(domainName, domainNode);
}
if (!errorSink.isEmpty())
{
ArrayNode errorArray = objectMapper.createArrayNode();
for (String errMsg : errorSink)
{
errorArray.add(errMsg);
}
outerNode.set("errors", errorArray);
}
return outerNode.toString();
}
}