LegacyWebSecurityManager.java
/*
** Module : LegacyWebSecurityManager.java
** Abstract : Security methods supporting legacy web (SOAP, REST, WebServiceHandler).
**
** Copyright (c) 2023-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ------------------------------------Description-------------------------------------------
** 001 GBB 20230825 Initial version.
** 002 RAA 20240604 Added an executeInContext method with useInitial parameter.
*/
/*
** 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.security;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.logging.*;
import static com.goldencode.p2j.security.SecurityManager.*;
/**
** Wrapper class for all method related to legacy services.
* {@link com.goldencode.p2j.main.WebServiceHandler}
* {@link com.goldencode.p2j.soap.SoapHandler}
* {@link com.goldencode.p2j.rest.RestHandler}
* <p>
* Using it through {@link SecurityManager#legacyWebSm} allows resetting SecurityManager instance.
*/
public class LegacyWebSecurityManager
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(LegacyWebSecurityManager.class);
/** SecurityManager instance. */
private final SecurityManager sm;
/**
* Public constructor. Accepts a SecurityManager instance to avoid deadlocks.
*
* @param sm
* SecurityManager instance.
*/
LegacyWebSecurityManager(SecurityManager sm)
{
this.sm = sm;
}
/**
* The map of currently active web requests, with their context data. The key is the token resolved after
* the initial authentication was performed.
*/
private Map<String, WebRequestContext> webRequestContexts = new ConcurrentHashMap<>();
/**
* A thread used to cleanup the expired {@link #webRequestContexts}. Only contexts which are not active
* being used will be cleaned.
*/
private Thread webServiceContextCleaner = null;
/**
* The work which does the authentication and authorization requires a server context. As it can
* originate either from the web request thread or from the web service worker thread, an
* {@link AssociatedThread} will be started if any web services has enabled authentication, and this is
* where the work will be performed.
*/
private WebServiceLogin webServiceLogin = null;
/**
* Check if the given credentials (user account and {@link UserAccount#getWebServiceToken() token}) is
* allowed to access FWD web services. Authorization will be done later, via {@link #checkAuthorization}.
*
* @param account
* The FWD user account.
* @param webServiceToken
* The web service token.
*
* @return <code>true</code> if there is a FWD user account with that name and web service token.
*
* @throws RestrictedUseException
* If this method is called from restricted context
*/
public boolean authWebRequest(String account, String webServiceToken)
throws RestrictedUseException
{
SecurityUtil.checkCallerAbort("com.goldencode.p2j.main.BasicAuth.authenticate");
if (!sm.isServer())
{
return false;
}
// safely query cached data
SecurityCache sc = sm.getCache();
Account acc = sc.getAccountById(account);
if (acc == null || acc.getAccountType() != Account.ACC_USER)
{
return false;
}
if (!webServiceToken.equals(((UserAccount) acc).getWebServiceToken()))
{
return false;
}
// we allow tracking of these users.
Object[] res = sm.allowAccount(null, acc.getSubjectId(), true);
return res == null;
}
/**
* Authenticate and create the FWD context
*
* @param account
* The FWD user account.
* @param webServiceToken
* The web service token.
* @param timeout
* The timeout after which the FWD context will be {@link #destroyWebRequestContext destroyed}.
* If set to zero, the context will live until a logout operation is performed.
*
* @return The authentication token which can be used for other calls.
*
* @throws RestrictedUseException
* If this method is called from restricted context
*/
public String createWebRequestContext(String account, String webServiceToken, int timeout)
throws RestrictedUseException
{
SecurityUtil.checkCallerAbort("com.goldencode.p2j.main.BasicAuth.login");
if (!sm.isServer())
{
return null;
}
// safely query cached data
SecurityCache sc = sm.getCache();
Account acc = sc.getAccountById(account);
if (acc == null || acc.getAccountType() != Account.ACC_USER)
{
return null;
}
if (!webServiceToken.equals(((UserAccount) acc).getWebServiceToken()))
{
return null;
}
RestrictedUseException[] errors = new RestrictedUseException[1];
CountDownLatch wait = new CountDownLatch(1);
String[] token = new String[1];
Runnable login = () ->
{
try
{
ContextSwitcher switcher = new ContextSwitcher();
token[0] = UUID.randomUUID().toString();
WebRequestContext context = new WebRequestContext(switcher, token[0], account, timeout);
// we allow tracking of these users.
Object[] res;
try
{
res = sm.postAuthenticateWorker(context, null, acc.getSubjectId(), true);
}
catch (RestrictedUseException e)
{
token[0] = null;
errors[0] = e;
return;
}
int authResult = (int) res[0];
if (authResult != AUTH_RESULT_SUCCESS)
{
token[0] = null;
return;
}
SecurityContext newContext = (SecurityContext) res[3];
if (newContext == null || sc != sm.getCache())
{
LOG.finer("createWebRequestContext: newer security cache generation available; " +
"authentication aborted.");
token[0] = null;
return;
}
webRequestContexts.put(token[0], context);
}
finally
{
wait.countDown();
}
};
// this needs to be executed on a server context thread
webServiceLogin.addTask(login);
try
{
wait.await();
}
catch (InterruptedException e)
{
// ignore
}
if (errors[0] != null)
{
throw errors[0];
}
return token[0];
}
/**
* Start the {@link #webServiceContextCleaner} and {@link #webServiceLogin} threads, using the server
* context.
*/
public void startWebServiceContextThreads()
{
if (sm.isServer())
{
// create the thread for cleaning the web request contexts
webServiceContextCleaner = new AssociatedThread(this::cleanExpiredWebRequestContexts);
webServiceContextCleaner.setName("WebServiceContextCleaner");
webServiceContextCleaner.setDaemon(true);
webServiceContextCleaner.start();
webServiceLogin = new WebServiceLogin();
Thread t = new AssociatedThread(webServiceLogin);
t.setName("WebServiceLogin");
t.setDaemon(true);
t.start();
}
}
/**
* Destroy the context for the web request identified by the specified token.
*
* @param token
* The authentication token.
*
* @return <code>true</code> if {@link LegacyWebSecurityManager#webRequestContexts} has this token and
* the FWD session was terminated.
*/
public boolean destroyWebRequestContext(String token)
{
WebRequestContext context = webRequestContexts.remove(token);
if (context == null)
{
return false;
}
try
{
sm.sessionSm.terminateSessionById(context);
}
catch (RestrictedUseException e)
{
LOG.log(LOG.isLoggable(Level.FINE) ? Level.FINE : Level.WARNING,
"Could not destroy context for token: " + token,
e);
return false;
}
return true;
}
/**
* Check if the specified token is associated with an active and not expired web request context.
* <p>
* If the web request is expired, it will be {@link #destroyWebRequestContext destroyed}.
*
* @param token
* The authentication token.
*
* @return <code>true</code> if the token exists and the web request is not expired.
*/
public boolean hasWebRequestContext(String token)
{
WebRequestContext context = webRequestContexts.get(token);
if (context != null)
{
if (context.isExpired())
{
destroyWebRequestContext(token);
return false;
}
return true;
}
return false;
}
/**
* Execute the specified task in the context associated with the web request identified via the specified
* authentication token.
*
* @param token
* The authentication token.
* @param task
* The task to be performed.
*/
public void executeInContext(String token, Runnable task)
{
executeInContext(token, task, false);
}
/**
* Execute the specified task in the context associated with the web request identified via the specified
* authentication token.
*
* @param token
* The authentication token.
* @param task
* The task to be performed.
* @param useInitial
* Flag indicating to create an initial security context.
*/
public void executeInContext(String token, Runnable task, boolean useInitial)
{
WebRequestContext context = webRequestContexts.get(token);
if (context == null)
{
return;
}
ContextSwitcher switcher = context.getContextSwitcher();
try
{
switcher.setContext(context, useInitial);
task.run();
}
finally
{
switcher.releaseContext(useInitial);
}
}
/**
* Execute the given task in the {@link LegacyWebSecurityManager#webServiceLogin} thread, so that it
* can check if the web request identified with the given authentication token is authorized for that
* web request.
*
* @param token
* The authentication token.
* @param task
* The authorization task.
*
* @return <code>true</code> if the web request token is authorized.
*/
public boolean checkAuthorization(String token, Supplier<Boolean> task)
{
WebRequestContext context = webRequestContexts.get(token);
if (context == null)
{
return false;
}
CountDownLatch wait = new CountDownLatch(1);
boolean[] res = new boolean[1];
Runnable work = () ->
{
ContextSwitcher switcher = context.getContextSwitcher();
try
{
switcher.setContext(context, false);
res[0] = task.get();
}
finally
{
switcher.releaseContext(false);
wait.countDown();
}
};
webServiceLogin.addTask(work);
try
{
wait.await();
}
catch (InterruptedException e)
{
// ignore
}
return res[0];
}
/**
* Given an authentication related code, return a {@link ContextSwitcher} instance, if the
* authentication was performed.
* <p>
* This switcher allows a thread to establish the context, if it knows a session ID.
*
* @param auth
* The authentication code.
*
* @return A context switcher instance.
*/
public ContextSwitcher getContextSwitcher(Runnable auth)
{
auth.run();
return sm.contextSm.hasContext() ? new ContextSwitcher() : null;
}
/**
* The main work performed by {@link #webServiceContextCleaner}, it goes through the
* {@link #webRequestContexts} every 10 seconds and cleans any contexts which are not in use and are
* expired.
*/
private void cleanExpiredWebRequestContexts()
{
Object lock = new Object();
while (true)
{
try
{
synchronized (lock)
{
// TODO: configure this?
lock.wait(10 * 1000); // 10 seconds wait
}
}
catch (InterruptedException e)
{
break;
}
List<String> destroy = new ArrayList<>();
for (WebRequestContext req : webRequestContexts.values())
{
if (req.isExpired() && !AppServerManager.isWebServiceTokenInUse(req.getToken()))
{
destroy.add(req.getToken());
}
}
for (String token : destroy)
{
destroyWebRequestContext(token);
}
}
}
/**
* A thread inheriting the FWD server context, used to perform authentication and authorization work for
* web requests.
*/
private static class WebServiceLogin
implements Runnable
{
/** The task to perform. */
private List<Runnable> tasks = new ArrayList<>();
/**
* Main work loop.
*/
public void run()
{
LinkedList<Runnable> work = new LinkedList<>();
while (true)
{
synchronized (this)
{
if (tasks.isEmpty())
{
try
{
wait();
}
catch (InterruptedException e)
{
// ignore
}
}
work.addAll(tasks);
tasks.clear();
}
for (Runnable task : work)
{
try
{
task.run();
}
catch (Throwable t)
{
// ignore
LOG.log(Level.SEVERE, "Error processing login task", t);
}
}
work.clear();
}
};
/**
* Add a task to the {@link #tasks queue}.
*
* @param task
* The task to perform.
*/
synchronized void addTask(Runnable task)
{
tasks.add(task);
this.notify();
}
}
}