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();
   }
}