REPO-5664/REPO-5665: System admin webscripts (#690)

This commit is contained in:
Jamal Kaabi-Mofrad
2021-09-02 13:59:38 +01:00
committed by GitHub
parent e854a01988
commit 3ce95c5262
9 changed files with 326 additions and 54 deletions

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2019 Alfresco Software Limited
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -30,6 +30,7 @@ import java.net.SocketException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletResponse;
@@ -355,7 +356,10 @@ public class RepositoryContainer extends AbstractRuntimeContainer
return;
}
if ((required == RequiredAuthentication.user || required == RequiredAuthentication.admin) && isGuest)
// if the required authentication is not equal to guest, then it should be one of the following:
// user | sysadmin | admin (the 'none' authentication is handled above)
// in this case the guest user should not be able to execute those scripts.
if (required != RequiredAuthentication.guest && isGuest)
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access.");
}
@@ -383,28 +387,9 @@ public class RepositoryContainer extends AbstractRuntimeContainer
{
return false;
}
// The user will now have been authenticated, based on HTTP Auth, Ticket etc
// The user will now have been authenticated, based on HTTP Auth, Ticket, etc.
// Check that the user they authenticated as has appropriate access to the script
// Check to see if they supplied HTTP Auth or Ticket as guest, on a script that needs more
if (required == RequiredAuthentication.user || required == RequiredAuthentication.admin)
{
final String authenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser();
final String runAsUser = AuthenticationUtil.getRunAsUser();
if ( (authenticatedUser == null) ||
(authenticatedUser.equals(runAsUser) && authorityService.hasGuestAuthority()) ||
(!authenticatedUser.equals(runAsUser) && authorityService.isGuestAuthority(authenticatedUser)) )
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires user authentication; however, a guest has attempted access.");
}
}
// Check to see if they're admin or system on an Admin only script
if (required == RequiredAuthentication.admin && !(authorityService.hasAdminAuthority() || AuthenticationUtil.getFullyAuthenticatedUser().equals(AuthenticationUtil.getSystemUserName())))
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + desc.getId() + " requires admin authentication; however, a non-admin has attempted access.");
}
checkScriptAccess(required, desc.getId());
if (debug)
{
@@ -424,7 +409,7 @@ public class RepositoryContainer extends AbstractRuntimeContainer
// Execute Web Script if authentication passed
// The Web Script has its own txn management with potential runAs() user
transactionedExecuteAs(script, scriptReq, scriptRes);
transactionedExecuteAs(script, scriptReq, scriptRes, required);
}
finally
{
@@ -441,6 +426,65 @@ public class RepositoryContainer extends AbstractRuntimeContainer
}
}
private boolean isSystemUser()
{
return Objects.equals(AuthenticationUtil.getFullyAuthenticatedUser(), AuthenticationUtil.getSystemUserName());
}
private boolean isSysAdminUser()
{
return authorityService.hasSysAdminAuthority();
}
private boolean isAdmin()
{
return authorityService.hasAdminAuthority();
}
public final boolean isAdminOrSystemUser()
{
return isAdmin() || isSystemUser();
}
/**
* Check to see if they supplied HTTP Auth or Ticket as guest, on a script that needs more
*/
private void checkGuestAccess(RequiredAuthentication required, String scriptDescriptorId)
{
if (required == RequiredAuthentication.user || required == RequiredAuthentication.admin
|| required == RequiredAuthentication.sysadmin)
{
final String authenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser();
final String runAsUser = AuthenticationUtil.getRunAsUser();
if ((authenticatedUser == null) || (authenticatedUser.equals(runAsUser)
&& authorityService.hasGuestAuthority()) || (!authenticatedUser.equals(runAsUser)
&& authorityService.isGuestAuthority(authenticatedUser)))
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + scriptDescriptorId
+ " requires user authentication; however, a guest has attempted access.");
}
}
}
private void checkScriptAccess(RequiredAuthentication required, String scriptDescriptorId)
{
// first, check guest access
checkGuestAccess(required, scriptDescriptorId);
// Check to see if the user is sysAdmin, admin or system on a sysadmin scripts
if (required == RequiredAuthentication.sysadmin && !(isSysAdminUser() || isAdminOrSystemUser()))
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + scriptDescriptorId
+ " requires system-admin authentication; however, a non-system-admin has attempted access.");
}
else if (required == RequiredAuthentication.admin && !isAdminOrSystemUser())
{
throw new WebScriptException(HttpServletResponse.SC_UNAUTHORIZED, "Web Script " + scriptDescriptorId
+ " requires admin authentication; however, a non-admin has attempted access.");
}
}
/**
* Execute script within required level of transaction
*
@@ -626,6 +670,35 @@ public class RepositoryContainer extends AbstractRuntimeContainer
}, runAs);
}
}
/**
* Execute script within required level of transaction as required effective user.
*
* @param script WebScript
* @param scriptReq WebScriptRequest
* @param scriptRes WebScriptResponse
* @param requiredAuthentication Required authentication
* @throws IOException
*/
private void transactionedExecuteAs(final WebScript script, final WebScriptRequest scriptReq,
final WebScriptResponse scriptRes, RequiredAuthentication requiredAuthentication) throws IOException
{
// Execute as System if and only if, the current user is a member of System-Admin group, and he is not a super admin.
// E.g. if 'jdoe' is a member of ALFRESCO_SYSTEM_ADMINISTRATORS group, then the work should be executed as System to satisfy the ACL checks.
// But, if the current user is Admin (i.e. super admin, which by default he is a member fo the ALFRESCO_SYSTEM_ADMINISTRATORS group)
// then don't wrap the work as RunAs, since he can do anything!
if (requiredAuthentication == RequiredAuthentication.sysadmin && isSysAdminUser() && !isAdmin())
{
AuthenticationUtil.runAs(() -> {
transactionedExecute(script, scriptReq, scriptRes);
return null;
}, AuthenticationUtil.SYSTEM_USER_NAME);
}
else
{
transactionedExecuteAs(script, scriptReq, scriptRes);
}
}
/* (non-Javadoc)
* @see org.alfresco.web.scripts.AbstractRuntimeContainer#onApplicationEvent(org.springframework.context.ApplicationEvent)

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -35,6 +35,7 @@ import org.alfresco.repo.jscript.ScriptUtils;
import org.alfresco.repo.web.scripts.RepositoryContainer;
import org.alfresco.service.cmr.admin.RepoUsage;
import org.alfresco.service.cmr.repository.StoreRef;
import org.springframework.extensions.webscripts.Description.RequiredAuthentication;
import org.springframework.extensions.webscripts.WebScript;
/**
@@ -65,27 +66,61 @@ public class WebScriptUtils extends ScriptUtils
*/
public Object[] findWebScripts(String family)
{
List<Object> values = new ArrayList<Object>();
List<Object> values = new ArrayList<>();
for (WebScript webscript : this.repositoryContainer.getRegistry().getWebScripts())
{
if (family != null)
addScriptDescription(family, values, webscript);
}
return values.toArray(new Object[0]);
}
/**
* Searches for webscript components with the given family name accessible to the current user.
*
* @param family the family
*
* @return An array of webscripts that match the given family name accessible to the current user
*
* @since 7.1
*/
public Object[] findWebScriptsForCurrentUser(String family)
{
List<Object> values = new ArrayList<>();
final boolean isAdminOrSystemUser = repositoryContainer.isAdminOrSystemUser();
for (WebScript webscript : this.repositoryContainer.getRegistry().getWebScripts())
{
final RequiredAuthentication required = webscript.getDescription().getRequiredAuthentication();
// Ignore admin webscripts if the current user is not an Admin or System
if (RequiredAuthentication.admin == required && !isAdminOrSystemUser)
{
Set<String> familys = webscript.getDescription().getFamilys();
if (familys != null && familys.contains(family))
{
values.add(webscript.getDescription());
}
continue;
}
else
addScriptDescription(family, values, webscript);
}
return values.toArray(new Object[0]);
}
private void addScriptDescription(String family, List<Object> values, WebScript webscript)
{
if (family != null)
{
Set<String> families = webscript.getDescription().getFamilys();
if (families != null && families.contains(family))
{
values.add(webscript.getDescription());
}
}
return values.toArray(new Object[0]);
else
{
values.add(webscript.getDescription());
}
}
public String getHostAddress()
{
try

View File

@@ -40,7 +40,7 @@ var Admin = Admin || {};
var toolInfo = {};
// collect the tools required for the Admin Console
var tools = utils.findWebScripts("AdminConsole");
var tools = utils.findWebScriptsForCurrentUser("AdminConsole");
// process each tool and generate the data so that a label+link can
// be output by the component template for each tool required

View File

@@ -7,7 +7,7 @@
<!-- COMMUNITY ONLY -->
<family>AdminConsole:Edition:Community</family>
<format default="html">argument</format>
<authentication>admin</authentication>
<authentication>sysadmin</authentication>
<lifecycle>internal</lifecycle>
<transaction allow="readonly">required</transaction>
</webscript>
</webscript>

View File

@@ -5,7 +5,7 @@
<url>/admin/</url>
<family>AdminConsoleHelper</family>
<format default="html">argument</format>
<authentication>admin</authentication>
<authentication>sysadmin</authentication>
<lifecycle>internal</lifecycle>
<transaction allow="readonly">required</transaction>
</webscript>
</webscript>

View File

@@ -5,7 +5,7 @@
</description>
<url>/api/admin/jmxdump</url>
<family>AdminConsoleHelper</family>
<authentication>admin</authentication>
<authentication>sysadmin</authentication>
<transaction allow="readonly"/>
<lifecycle>internal</lifecycle>
</webscript>
</webscript>

View File

@@ -3,8 +3,8 @@
<description>Update and retrieve repository usage</description>
<url>/api/admin/usage</url>
<format default="json" />
<authentication>admin</authentication>
<authentication>sysadmin</authentication>
<transaction>required</transaction>
<family>Admin</family>
<lifecycle>internal</lifecycle>
</webscript>
</webscript>

View File

@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -31,10 +31,12 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.dictionary.Facetable;
import org.alfresco.repo.dictionary.IndexTokenisationMode;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authority.AuthorityServiceImpl;
import org.alfresco.repo.web.scripts.BaseWebScriptTest;
import org.alfresco.service.cmr.admin.RepoAdminService;
import org.alfresco.service.cmr.admin.RepoUsage;
@@ -48,11 +50,18 @@ import org.alfresco.service.cmr.dictionary.ModelDefinition;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.i18n.MessageLookup;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.MutableAuthenticationService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.descriptor.DescriptorService;
import org.alfresco.service.license.LicenseDescriptor;
import org.alfresco.service.namespace.QName;
import org.alfresco.test_category.OwnJVMTestsCategory;
import org.alfresco.util.PropertyMap;
import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.springframework.context.ApplicationContext;
@@ -74,29 +83,46 @@ import static org.mockito.Mockito.when;
@Category(OwnJVMTestsCategory.class)
public class AdminWebScriptTest extends BaseWebScriptTest
{
private ApplicationContext ctx;
private RepoAdminService repoAdminService;
private DescriptorService descriptorService;
private RepoAdminService repoAdminService;
private DescriptorService descriptorService;
private PersonService personService;
private MutableAuthenticationService authenticationService;
private String admin;
private String guest;
private String user1_sysAdmin;
private String user2;
@Override
protected void setUp() throws Exception
{
super.setUp();
ctx = getServer().getApplicationContext();
repoAdminService = (RepoAdminService) ctx.getBean("RepoAdminService");
descriptorService = (DescriptorService) ctx.getBean("DescriptorService");
ApplicationContext ctx = getServer().getApplicationContext();
repoAdminService = ctx.getBean("RepoAdminService", RepoAdminService.class);
descriptorService = ctx.getBean("DescriptorService", DescriptorService.class);
personService = ctx.getBean("PersonService", PersonService.class);
authenticationService = ctx.getBean("AuthenticationService", MutableAuthenticationService.class);
AuthorityService authorityService = ctx.getBean("AuthorityService", AuthorityService.class);
admin = AuthenticationUtil.getAdminUserName();
guest = AuthenticationUtil.getGuestUserName();
AuthenticationUtil.setFullyAuthenticatedUser(admin);
user1_sysAdmin = RandomStringUtils.randomAlphabetic(10);
String user1_password = RandomStringUtils.randomAlphabetic(10);
createUser(user1_sysAdmin, user1_password);
authorityService.addAuthority(AuthorityServiceImpl.GROUP_ALFRESCO_SYSTEM_ADMINISTRATORS_AUTHORITY, user1_sysAdmin);
user2 = RandomStringUtils.randomAlphabetic(10);
String user2_password = RandomStringUtils.randomAlphabetic(10);
createUser(user2, user2_password);
}
@Override
protected void tearDown() throws Exception
{
super.tearDown();
AuthenticationUtil.clearCurrentSecurityContext();
}
public void testGetRestrictions() throws Exception
@@ -227,6 +253,129 @@ public class AdminWebScriptTest extends BaseWebScriptTest
assertTrue(property.getResidual());
}
public void testSysAdminAccess() throws Exception
{
AuthenticationUtil.clearCurrentSecurityContext();
String url = "/admin/admin-communitysummary";
TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url);
Response response = sendRequest(req, Status.STATUS_OK, user1_sysAdmin);
Document doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("System Summary"));
// Super Admin should still have access to all the scripts
response = sendRequest(req, Status.STATUS_OK, admin);
doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("System Summary"));
}
public void testSysAdminAccess_nodeBrowser() throws Exception
{
AuthenticationUtil.clearCurrentSecurityContext();
String nodeBrowserUrl = "/admin/admin-nodebrowser";
// test the get webscript of the node browser
TestWebScriptServer.GetRequest getReq = new TestWebScriptServer.GetRequest(nodeBrowserUrl);
// The node browser is only accessible to admins, not sysAdmins
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// test the post webscript of the node browser too
TestWebScriptServer.PostRequest postReq = new TestWebScriptServer.PostRequest(nodeBrowserUrl, "",
"multipart/form-data; boundary=----WebKitFormBoundaryjacWCXfJ3KjtRenA");
// The node browser is only accessible to admins, not sysAdmins
sendRequest(postReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// Normal user shouldn't have access either
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user2);
// Admin should have access to everything
Response response = sendRequest(getReq, Status.STATUS_OK, admin);
Document doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("Node Browser"));
}
public void testSysAdminAccess_repoConsole() throws Exception
{
String repoConsoleUrl = "/admin/admin-repoconsole";
// test the get webscript of the repo console
TestWebScriptServer.GetRequest getReq = new TestWebScriptServer.GetRequest(repoConsoleUrl);
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// test the post webscript of the repo console too
TestWebScriptServer.PostRequest postReq = new TestWebScriptServer.PostRequest(repoConsoleUrl, "",
"multipart/form-data; boundary=----WebKitFormBoundaryjacWCXfJ3KjtRenA");
sendRequest(postReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// Normal user shouldn't have access either
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user2);
// Admin should have access to everything
Response response = sendRequest(getReq, Status.STATUS_OK, admin);
Document doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("Model and Messages Console"));
}
public void testSysAdminAccess_tenantConsole() throws Exception
{
String tenantConsoleUrl = "/admin/admin-tenantconsole";
// test the get webscript of the tenant console
TestWebScriptServer.GetRequest getReq = new TestWebScriptServer.GetRequest(tenantConsoleUrl);
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// test the post webscript of the tenant console too
TestWebScriptServer.PostRequest postReq = new TestWebScriptServer.PostRequest(tenantConsoleUrl, "",
"multipart/form-data; boundary=----WebKitFormBoundaryjacWCXfJ3KjtRenA");
sendRequest(postReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// Normal user shouldn't have access either
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user2);
// Admin should have access to everything
Response response = sendRequest(getReq, Status.STATUS_OK, admin);
Document doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("Tenant Admin Console"));
}
public void testSysAdminAccess_workflowConsole() throws Exception
{
String workflowConsoleUrl = "/admin/admin-workflowconsole";
// test the get webscript of the workflow console
TestWebScriptServer.GetRequest getReq = new TestWebScriptServer.GetRequest(workflowConsoleUrl);
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// test the post webscript of the workflow console too
TestWebScriptServer.PostRequest postReq = new TestWebScriptServer.PostRequest(workflowConsoleUrl, "",
"multipart/form-data; boundary=----WebKitFormBoundaryjacWCXfJ3KjtRenA");
sendRequest(postReq, Status.STATUS_UNAUTHORIZED, user1_sysAdmin);
// Normal user shouldn't have access either
sendRequest(getReq, Status.STATUS_UNAUTHORIZED, user2);
// Admin should have access to everything
Response response = sendRequest(getReq, Status.STATUS_OK, admin);
Document doc = Jsoup.parse(response.getContentAsString());
assertNotNull(doc.title());
assertTrue(doc.title().contains("Workflow Admin Console"));
}
public void testNonSysAdminAccess() throws Exception
{
AuthenticationUtil.clearCurrentSecurityContext();
String url = "/admin/admin-communitysummary";
TestWebScriptServer.GetRequest req = new TestWebScriptServer.GetRequest(url);
sendRequest(req, Status.STATUS_UNAUTHORIZED, user2);
}
private class SimplePropertyDefinition implements PropertyDefinition
{
private boolean isAspect;
@@ -350,4 +499,19 @@ public class AdminWebScriptTest extends BaseWebScriptTest
return null;
}
}
private void createUser(String username, String password)
{
if (!personService.personExists(username))
{
this.authenticationService.createAuthentication(username, password.toCharArray());
PropertyMap personProps = new PropertyMap();
personProps.put(ContentModel.PROP_USERNAME, username);
personProps.put(ContentModel.PROP_FIRSTNAME, "testFirstName");
personProps.put(ContentModel.PROP_LASTNAME, "testLastName");
personProps.put(ContentModel.PROP_EMAIL, username + "@email.com");
this.personService.createPerson(personProps);
}
}
}