Saturday, August 12, 2017

Downloading content from alfresco repository via custom Download Webscript


Alfresco provides Content REST API for downloading the content from repository as given below.

GET /alfresco/service/api/node/content/{store_type}/{store_id}/{id}?a=false&alf_ticket={TICKET}

Example:

http://127.0.0.1:8080/alfresco/service/api/node/content/workspace/SpacesStore/85d2a84d-271b-41b1-9449-02f5942893a0?a=false&alf_ticket=TICKET_5550fa5e9b87bead8f008e906185e023b7ce21ed

Where: 
a: attach. if true, force download of content as attachment. Possible values are true/false
store_type: workspace
store_id: SpacesStore
id: nodeRefId (UUID of the node)

A StoreRef is comprised of:

Store Protocol - that is, the type of store
Store Identifier - the id of the store

Example storeRefs are:

workspace://SpacesStore  (store_type://store_id)
version://versionStore  (store_type://store_id)
archive://SpacesStore  (store_type://store_id)

See here for Content API Webscript definition: content.get.desc.xml

Any external system or client can use this API to download content from Alfresco repository via an authenticated user which would be registered in Alfresco.

If you want to know how to get auth ticket (alf_ticket) then visit: Alfresco Login REST API

OOTB Download REST API will allow to download the content to any user who is registered in alfresco, since every user has consumer access to every site by default via "EVERYONE" group. But let's suppose you want to put some kind of restrictions to the Download API. Let's say for example:

1- Allow download if requesting user is authorized to download the content.
2- Want to validate the site level user role e.g. only Managers/Collaborators/Contributors can download, Consumers should not be allowed to download.
3- Want to check if user is part of DOWNLOADERS groups then allow them to download 
..... 
and so on. 

There could be many such cases which we can not achieve via OOTB REST API provided by alfresco. If your contents has copyrights you will definitely not allow users to download the content who are unauthorized. 

To handle such scenarios you need to write a custom Download webscript.

Alfresco provides a Webscript called "org.alfresco.repo.web.scripts.content.StreamContent". 

By extending this class we can add our custom user validation logic and leave the streaming and download handling part to this OOTB Webscript.

So, let's take a use case where you don't want a consumer user to download the content from a site. To achieve this use case a custom webscript will be written as given below:


DownloadContentWebscript.java


/*
 * Created By: Abhinav Kumar Mishra
 * Copyright © 2017. Abhinav Kumar Mishra. 
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.abhinavmishra14.webscript;

import java.io.IOException;
import java.util.Locale;
import java.util.Set;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.web.scripts.content.StreamContent;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.security.AccessPermission;
import org.alfresco.service.cmr.security.AuthenticationService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.site.SiteInfo;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.namespace.QName;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;
import org.springframework.extensions.webscripts.WebScriptResponse;

/**
 * The Class DownloadContentWebscript.
 * 
 */
public class DownloadContentWebscript extends StreamContent {

 /** The Constant LOGGER. */
 private static final Logger LOGGER = LoggerFactory.getLogger(DownloadContentWebscript.class);

 /** The content service. */
 private ContentService contentService;

 /** The authentication service. */
 private AuthenticationService authenticationService;

 /** The site service. */
 private SiteService siteService;

 /** The authority service. */
 private AuthorityService authorityService;

 /* (non-Javadoc)
  * @see org.springframework.extensions.webscripts.WebScript#execute(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.WebScriptResponse)
  */
 @Override
 public void execute(final WebScriptRequest request,
   final WebScriptResponse response) throws IOException {
  LOGGER.info("Started executing DownloadContentWebscript...");
  try {
   final NodeRef nodeRef = getParameterAsNodeRef(request, "nodeRef");
   final String userName = authenticationService.getCurrentUserName();
   if(isNotAuthorised(nodeRef, userName, siteService, permissionService, authorityService)) {
    response.setStatus(401);
    response.getWriter().write("User is unauthorised to download the requested content!");
   } else {
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Processing the download requested by: {}", userName);
    }
    final boolean attach = Boolean.valueOf(request.getParameter("attach"));
    processDownload(request, response, nodeRef, attach, ContentModel.PROP_CONTENT);
   }
  } catch (AccessDeniedException accessDenied) {
   LOGGER.error("Access denied while downloading content", accessDenied);
   throw new WebScriptException(Status.STATUS_UNAUTHORIZED,
     accessDenied.getMessage(), accessDenied);
  } catch (IOException | AlfrescoRuntimeException
    | InvalidNodeRefException excp) {
   LOGGER.error("Exception occurred while downloading content", excp);
   throw new WebScriptException(Status.STATUS_INTERNAL_SERVER_ERROR,
     excp.getMessage(), excp);
  }
  LOGGER.info("Existing from DownloadContentWebscript...");
 }

 /**
  * Process download.
  *
  * @param request the request
  * @param response the response
  * @param nodeRef the node ref
  * @param attach the attach
  * @param propertyQName the property q name
  * @throws IOException the IO exception
  */
 private void processDownload(final WebScriptRequest request,
   final WebScriptResponse response, final NodeRef nodeRef, final boolean attach,
   final QName propertyQName) throws IOException {
  String userAgent = request.getHeader("User-Agent");
  userAgent = StringUtils.isNotBlank(userAgent) ? userAgent.toLowerCase(Locale.ENGLISH) : StringUtils.EMPTY;
  final boolean isClientSupported= userAgent.contains("msie")
    || userAgent.contains(" trident/")
    || userAgent.contains(" chrome/")
    || userAgent.contains(" firefox/");

  if (attach && isClientSupported) {
   String fileName = (String) this.nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
   if (userAgent.contains("msie") || userAgent.contains(" trident/")) {
    final String mimeType = contentService.getReader(nodeRef, propertyQName).getMimetype();
    if (!(this.mimetypeService.getMimetypes(FilenameUtils.getExtension(fileName)).contains(mimeType))) {
     fileName = FilenameUtils.removeExtension(fileName)+ FilenameUtils.EXTENSION_SEPARATOR_STR
       + this.mimetypeService.getExtension(mimeType);
    }
   }
   streamContent(request, response, nodeRef, propertyQName, attach, fileName, null);
  } else {
   streamContent(request, response, nodeRef, propertyQName, attach, null, null);
  }
 }

 /**
     * Create NodeRef instance from a WebScriptRequest parameter.
     *
     * @param req the req
     * @param paramName the param name
     * @return the parameter as node ref
     */
    private NodeRef getParameterAsNodeRef(final WebScriptRequest req, final String paramName) {
        final String nodeRefStr = StringUtils.trimToNull(req.getParameter(paramName));
        if (StringUtils.isBlank(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Missing " + paramName + " parameter");
        }
        if (!NodeRef.isNodeRef(nodeRefStr)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Incorrect format for " + paramName + " paramater");
        }
        final NodeRef nodeRef = new NodeRef(nodeRefStr);
        if (!nodeService.exists(nodeRef)) {
            throw new WebScriptException(Status.STATUS_BAD_REQUEST, paramName + " not found");
        }
        return nodeRef;
    }
   
 /**
  * Checks if is not authorised.
  *
  * @param nodeRef the node ref
  * @param userName the user name
  * @param siteService the site service
  * @param permissionService the permission service
  * @param authorityService the authority service
  * @return true, if checks if is not authorised
  */
 private boolean isNotAuthorised(final NodeRef nodeRef,
   final String userName, final SiteService siteService,
   final PermissionService permissionService,
   final AuthorityService authorityService) {
  boolean isNotAuthorised = false;
  final SiteInfo siteInfo = siteService.getSite(nodeRef);
  // Checking siteInfo, If it is null that means user is not a member of site and 
  // hence isNotAuthorised is default to false.
  if (null != siteInfo) {
   if (siteService.isMember(siteInfo.getShortName(), userName)) {
    final Set<AccessPermission> permissions = permissionService.getAllSetPermissions(nodeRef);
    if(LOGGER.isDebugEnabled()) {
     LOGGER.debug("Checking isNotAuthorised, Available access permissions are: {}", permissions);
    }
    for (final AccessPermission permission : permissions) {
     if (permission.getPermission().equals("SiteConsumer")
       || permission.getPermission().equals("Consumer")) {
      if (permission.getAuthorityType().equals("USER")
        && permission.getAuthority().equals(userName)) {
       isNotAuthorised = true;
       break;
      } else if (permission.getAuthorityType().toString().equals("GROUP")) {
       //Set run as system user since other users including consumers can not fetch authorities
       AuthenticationUtil.setRunAsUserSystem();
       final Set<String> authorities = authorityService.getAuthoritiesForUser(userName);
       //Clear system user context and set original user context
       AuthenticationUtil.clearCurrentSecurityContext();
       AuthenticationUtil.setFullyAuthenticatedUser(userName);
       if(LOGGER.isDebugEnabled()) {
        LOGGER.debug("Checking permissions at GROUP level, user has following authorities: {}", authorities);
       }
       for (final String authority : authorities) {
        if (authority.equals(permission.getAuthority())) {
         isNotAuthorised = true;
         break;
        }
       }
      }
     }
    }
   } else {
    isNotAuthorised = true;//Not a member in the site.
   }
  }
  return isNotAuthorised;
 }
 
 /**
  * Sets the content service.
  *
  * @param contentService the content service
  */
 public void setContentService(final ContentService contentService) {
  this.contentService = contentService;
 }

 /**
  * Sets the authentication service.
  *
  * @param authenticationService the authentication service
  */
 public void setAuthenticationService(final AuthenticationService authenticationService) {
  this.authenticationService = authenticationService;
 }

 /**
  * Sets the site service.
  *
  * @param siteService the site service
  */
 public void setSiteService(final SiteService siteService) {
  this.siteService = siteService;
 }

 /**
  * Sets the authority service.
  *
  * @param authorityService the authority service
  */
 public void setAuthorityService(final AuthorityService authorityService) {
  this.authorityService = authorityService;
 }
}

WebScript Description:
alfresco/extension/templates/webscripts/com/github/abhinavmishra14/downloadContent.get.desc.xml


<webscript>
 <shortname>Download Content</shortname>
 <description>
  <![CDATA[
      Download Content based on role and permissions check.
      Where:
      nodeRef- NodeRef of the content e.g. workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e
      attach (Optional)- if true, force download of content as attachment (Possible values are true/false)
      
      Sample URL:
      http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true
   ]]>
 </description>
 <url>/abhinavmishra14/downloadContent?nodeRef={nodeRef}&amp;attach={attach?}</url>
 <format default="">argument</format>
 <authentication>user</authentication>
 <transaction allow="readonly" />
 <family>common</family>
</webscript>


Bean definition:
webscript-context.xml


<bean id="webscript.com.github.abhinavmishra14.downloadContent.get" class="com.github.abhinavmishra14.webscript.DownloadContentWebscript" parent="webscript">
  <property name="permissionService" ref="PermissionService" />
  <property name="nodeService" ref="NodeService" />
  <property name="mimetypeService" ref="MimetypeService" />
  <property name="delegate" ref="webscript.content.streamer" />
  <property name="repository" ref="repositoryHelper" />
  
  <property name="contentService" ref="ContentService" />
  <property name="authenticationService" ref="AuthenticationService" />
  <property name="siteService" ref="SiteService" />
  <property name="authorityService" ref="AuthorityService"/>
 </bean>


To test the above web-script it is assumed that you have a site where there are users with SiteConsumer/Consumer role.


Test URL:

GET http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true

OR

GET http://127.0.0.1:8080/alfresco/service/abhinavmishra14/downloadContent?nodeRef=workspace://SpacesStore/5cee9f74-eb2a-43a4-965d-6e4fcde4fb9e&attach=true&alf_ticket=TICKET_4e3c094e886a54893a267cd3cac402c1cc5b4fd9 



Note: Custom download webscript can also be implemented using org.springframework.extensions.webscripts.AbstractWebScript but streaming logic would have to be written by developer itself. Like setting the stream to response, preparing the attachment etc. 

For more on upload/delete/download visit my post here: Upload/Download/Delete REST APIs


Friday, June 23, 2017

Add/Remove aspects on a node in alfresco


There is an out of the box web script which Alfresco uses internally to add remove aspects. This web script can be seen in browser console when you go to "Manage Aspect" action and select a set of aspects to add/remove.

I came across an issue while working, where i had to remove a set of aspects and then add a set of aspects.

Initially i thought of writing a custom WebScript but got idea to see how "Manage Aspect" action is working? Can i utilize same back-end script to handle my case?

And guess what, it worked well :)

So let me give the little detail about the WebScript which Alfresco uses under "Manage Aspect" action.

WebScript:

[POST] http://<host:port>/share/proxy/alfresco/slingshot/doclib/action/aspects/node/{store_type}/{store_id}/{id}

where,

store_type: workspace
store_id: SpacesStore
id: nodeRefId (UUID of the node)

Just to set the context let me tell little bit about store_type, store_id and id.

store_type and store_id combined is called StoreRef. store_type , store_id and id will construct nodeRef.

A StoreRef is comprised of:

Store Protocol - that is, the type of store
Store Identifier - the id of the store

Example storeRefs are:

workspace://SpacesStore  (store_type://store_id)
version://versionStore  (store_type://store_id)
archive://SpacesStore   (store_type://store_id)

So, let's try out above given WebScript.

Suppose i have a node (workspace://SpacesStore/49cd3638-b0b2-40d8-ae14-a40cf7fad3b1) where i want to remove a set of aspects and then add another set of aspects.

We have to prepare URL like given below and send the POST request with below given JSON payload.

URL:

[POST] http://<host:port>/share/proxy/alfresco/slingshot/doclib/action/aspects/node/{store_type}/{store_id}/{id}


http://1270.0.0.1:8081/share/proxy/alfresco/slingshot/doclib/action/aspects/node/workspace/SpaceStore/49cd3638-b0b2-40d8-ae14-a40cf7fad3b1


To remove a set of aspects and add a set of aspects, we can use below given payload:
{
    "added":["pubModel:publishable", "pubModel:webable"],
    "removed":["cm:versionable", "exif:exif"]
}

To add a set of aspects, we can use below given payload:
{
    "added":["pubModel:publishable", "pubModel:webable"],
    "removed":[]
}

To remove a set of aspects, we can use below given payload:
{
   "added":[],
   "removed":["cm:versionable", "exif:exif"]
}

If operation is successful, then you will get below JSON response.
{
   "totalResults": 1,
   "overallSuccess": true,
   "successCount": 1,
   "failureCount": 0,
   "results":
   [
      {
         "id": "Assets",
         "action": "manageAspects",
         "nodeRef": "workspace:\/\/SpacesStore\/49cd3638-b0b2-40d8-ae14-a40cf7fad3b1",
         "type": "folder",
         "success": true
      }
   ]
}




If operation is unsuccessful (probably if aspect is invalid), then you will get below JSON response.
{
    "totalResults": 1,
    "overallSuccess": false,
    "successCount": 0,
    "failureCount": 1,
    "results": [
        {
            "id": "PO marc 3",
            "action": "manageAspects",
            "nodeRef": "workspace://SpacesStore/49cd3638-b0b2-40d8-ae14-a40cf7fad3b1",
            "type": "folder",
            "success": false
        }
    ]
}



Although above given WebScript is not the best way to do these operations, but as a workaround it is an option.





Friday, April 28, 2017

Extracting active logged-in users and active authentication tickets from Alfresco


Sometimes an Alfresco Administrator might need to track active logged-in users in the system for audit purpose or for planning maintenance activities. There can be many other use-cases depending of type of usage or organization policies.

Alfresco doesn't provide this kind of feature for admins out of the box as of now. However Alfresco 5.2 version on-wards they are providing Support Tools feature which can provide various options including this particular use-case.

Support Tools is an Add-On, which is available on GitHub as alfresco-support-tools which can be installed as a module as well. Alfresco 5.2 (enterprise) on-wards alfresco has integrated this add-on out of the box.

See here: Support Tools

Here we are going to use Alfresco's TicketComponent service to get the active user details and active ticket details.
We will create a java backed web-script which will return the basic details about the active users, total active user count, total no. of active tickets etc.

Follow the below given steps:

  • Create following folder structure under "/alfresco/extension/templates/webscripts" directory
    • Create a folder named "com" under "webscripts"
    • Under folder "com", create a folder named "github"
    • Under folder "github", create a folder named "abhinavmishra14"
    • Under folder "abhinavmishra14", create a folder named "audit"
<webscript>
	<shortname>Active Users</shortname>
	<description>This webscript returns the active users logged-in into
		Alfresco.
		Sample response:
		{
		activeTicketsCount: 2,
		activeUsers: "["admin","abhinav@gmail.com "]",
		activeUsersCount: 2,
		_comment: "Active user count may be lower than the ticket count, since a user
		can have more than one ticket/session. Ticket count may be higher than
		the active user count, since a user can have more than one
		ticket/session."
		}
	</description>
	<url>/audit/getActiveUsers</url>
	<format default="json" />
	<authentication>admin</authentication>
	<family>Audit</family>
</webscript>
 
  • Create a freemarker template “getActiveUsers.get.json.ftl” which is used to generate the view under "com > github > abhinavmishra14 > audit" folder.
<#escape x as jsonUtils.encodeJSONString(x)> ${response} </#escape>

  • Create a java package "com.github.abhinavmishra14.audit.webscript"
  • Create java class "GetActiveUsersWebscript" under the "com.github.abhinavmishra14.audit.webscript" package.
/*
 * Created By: Abhinav Kumar Mishra
 * Copyright &copy; 2017. Abhinav Kumar Mishra. 
 * All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.abhinavmishra14.audit.webscript;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.security.authentication.TicketComponent;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.extensions.webscripts.Cache;
import org.springframework.extensions.webscripts.DeclarativeWebScript;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.WebScriptException;
import org.springframework.extensions.webscripts.WebScriptRequest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * The Class GetActiveUsersWebscript.
 */
public class GetActiveUsersWebscript extends DeclarativeWebScript {

	/** The Constant LOGGER. */
	private static final Logger LOGGER = LoggerFactory.getLogger(GetActiveUsersWebscript.class);

	/** The Constant ACTIVE_USERS. */
	private static final String ACTIVE_USERS = "activeUsers";

	/** The Constant ACTIVE_USERS_COUNT. */
	private static final String ACTIVE_USERS_COUNT = "activeUsersCount";

	/** The Constant ACTIVE_TICKETS_COUNT. */
	private static final String ACTIVE_TICKETS_COUNT = "activeTicketsCount";

	/** The Constant COMMENT_DATA. */
	private static final String COMMENT_DATA = "_comment";

	/** The Constant RESPONSE. */
	private static final String RESPONSE = "response";

	/** The ticket component. */
	private final TicketComponent ticketComponent;

	/**
	 * The Constructor.
	 *
	 * @param ticketComponent the ticket component
	 */
	public GetActiveUsersWebscript(final TicketComponent ticketComponent) {
		super();
		this.ticketComponent = ticketComponent;
	}

	/* (non-Javadoc)
	 * @see org.springframework.extensions.webscripts.DeclarativeWebScript#executeImpl(org.springframework.extensions.webscripts.WebScriptRequest, org.springframework.extensions.webscripts.Status, org.springframework.extensions.webscripts.Cache)
	 */
	@Override
	public Map<String, Object> executeImpl(final WebScriptRequest req,
			final Status status, final Cache cache) {
		if(LOGGER.isDebugEnabled()) {
			LOGGER.debug("Extracting active users..");
		}
		final Map<String, Object> model = new ConcurrentHashMap<String, Object>(3);
		try {
			//get nonExpiredOnly users with tickets
			final Set<String> activeUsers = ticketComponent.getUsersWithTickets(true);
			final ObjectMapper objMapper = new ObjectMapper();
			objMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);

			if (activeUsers != null && !activeUsers.isEmpty()) {
				final JSONObject activeUsersJson = new JSONObject();
				//This may be lower than the ticket count, since a user can have more than one
				// ticket/session
				activeUsersJson.put(ACTIVE_USERS, objMapper.writeValueAsString(activeUsers));
				activeUsersJson.put(ACTIVE_USERS_COUNT, activeUsers.size());

				//This may be higher than the user count, since a user can have more than one   
				// ticket/session
				//get nonExpiredOnly ticket count
				activeUsersJson.put(ACTIVE_TICKETS_COUNT, ticketComponent.countTickets(true));

				activeUsersJson.put(COMMENT_DATA, "Active user count may be lower than the ticket count, since a user can have more than one ticket/session. Ticket count may be higher than the active user count, since a user can have more than one ticket/session.");
				model.put(RESPONSE, activeUsersJson);
			}
		} catch (JsonProcessingException | JSONException excp) {
			LOGGER.error("Exception occurred while preparing json for active users ", excp);
			throw new WebScriptException(
					Status.STATUS_INTERNAL_SERVER_ERROR, excp.getMessage(), excp);
		} catch (AlfrescoRuntimeException alfErr) {
			LOGGER.error("Unexpected error occurred while getting active users ", alfErr);
			throw new WebScriptException(
					Status.STATUS_INTERNAL_SERVER_ERROR, alfErr.getMessage(), alfErr);
		}
		if(LOGGER.isDebugEnabled()) {
			LOGGER.debug("Extracted active users.");
		}
		return model;
	}
}

<bean id="webscript.com.github.abhinavmishra14.audit.getActiveUsers.get"
	  class="com.github.abhinavmishra14.audit.webscript.GetActiveUsersWebscript"
	  parent="webscript">
  <constructor-arg ref="ticketComponent" />
</bean>

  • Build and start the alfresco instance.
  • It will return following type of response (example):
{
  "activeTicketsCount": 3,
  "activeUsersCount": 3,
  "_comment": "Active user count may be lower than the ticket count, since a user can have more than one ticket/session. Ticket count may be higher than the active user count, since a user can have more than one ticket/session.",
  "activeUsers": "[\"test2\",\"admin\",\"guest\"]"
}



Refer this github project in case you need to refer to the codehttps://github.com/abhinavmishra14/active-users-report

Alternatively, you can apply "active-users-report-1.0-SNAPSHOT.amp" to your alfresco installation as an add-on.