diff --git a/README.md b/README.md index d1bd032e..7f3cfdaf 100644 --- a/README.md +++ b/README.md @@ -1,428 +1,67 @@ Access Control Tool for Adobe Experience Manager ================================================ -The Access Control Tool for Adobe Experience Manager (ACTool) is a tool that simplifies the specification and deployment of complex [Access Control Lists in AEM] (http://docs.adobe.com/docs/en/cq/current/administering/security.html#Access%20Control%20Lists%20and%20how%20they%20are%20evaluated). +The Access Control Tool for Adobe Experience Manager (ACTool) simplifies the specification and deployment of complex [Access Control Lists in AEM] (http://docs.adobe.com/docs/en/cq/current/administering/security.html#Access%20Control%20Lists%20and%20how%20they%20are%20evaluated). +Instead of [existing solutions](docs/Comparison.md) that build e.g. a content package with actual ACL nodes you can write simple configuration files and deploy them with your content packages. -# Prerequisites +Features: +* compatible with AEM 6.x and CQ 5.6.1 +* easy-to-read Yaml configuration file format +* run mode support +* automatic installation with install hook +* cleans obsolete ACL entries when configuration is changed +* ACLs can be exported +* stores history of changes +* ensured order of ACLs +* built-in expression language to reduce rule duplication -Building the ACTool requires Java 7 and Maven 3.2. +# Requirements -Installing ACTool requires CQ5.6.1 (min. SP2)/AEM 6.0/AEM 6.1/AEM 6.2. +The ACTool requires Java 7 and AEM 6.0 - 6.2 or CQ 5.6.1 (min. SP2). # Installation -A full build of ACTool can be executed by running: +The package is available via [Maven](https://repo1.maven.org/maven2/biz/netcentric/cq/tools/accesscontroltool/accesscontroltool-package/). Install it e.g. via CRX package manager. ``` -mvn clean install -``` - -This command, if successful, will create a CQ Package as a ZIP file inside accesscontroltool-package/target called accesscontroltool-package-.zip. - -The package can be installed using the AEM Package Manager or directly from the command line, assuming AEM is running on host localhost, port 4502, using the command: - -``` -mvn -PautoInstallPackage install + biz.netcentric.cq.tools.accesscontroltool + accesscontroltool-package ``` ## AEM6.x/Oak -The `oakindex-package` contains an optimized Oak index to cover all queries being issued by the Access Control Tool. To build (and optionally deploy) the content-package use the Maven profile oakindex. This package is only compatible with Oak and even there it is optional (as it will only speed up queries). - -To use the package, run all commands with profile `oakindex`, e.g. - ``` -mvn clean install -Poakindex - ``` - -# Configuration File Format - -For better human readability and easy editing the ACL configuration files use the YAML format. - - -## Overall structure a of an AC configuration file - - - -Every configuration file comprises a group section where groups and their membership to other groups get defined and a ACE section where all ACEs in the repository regarding these groups get defined. These ACE definitions are again written under the respective group id. The group of an ACE definition in each configuration file has to match a group which is also defined in the same file. Groups which are contained in the isMemberOf property within a group definition have either to be defined in another configuration file or already be installed in the system on which the installation takes place. - -## Configuration of groups - -A authorizable record in the configuration file starts with the principal id followed by some indented data records containing properties like name/description and group membership: - -``` -[Groupd Id] - - name: groupname (optional, if empty groupd id is taken) - - isMemberOf: comma separated list of other groups - - members: comma separated list of groups that are member of this group - - description: (optional, description) - - path: (optional, path of the group in JCR) - - migrateFrom: (optional, a group name assigned member users are taken over from, since v1.7) -``` +In case you run AEM 6 with Oak (required as of 6.1) we recommend to install the Oak index package. +It will speed up installation of ACLs. -Example +You can get the ZIP file via [Maven](https://repo1.maven.org/maven2/biz/netcentric/cq/tools/accesscontroltool/accesscontroltool-oakindex-package/). Install it e.g. via CRX package manager. ``` -isp-editor - - isMemberOf: fragment-restrict-for-everyone,fragment-allow-nst - - members: editor + biz.netcentric.cq.tools.accesscontroltool + accesscontroltool-oakindex-package ``` -If the isMemberOf property of a group contains a group which is not yet installed in the repository, this group gets created and its rep:members property gets filled accordingly. if another configuration gets installed having a actual definition for that group the data gets merged into the already existing one. +# Configuration of ACL entries -The members property contains a list of groups where this group is added as isMemberOf. +You need to setup [Yaml configuration files](docs/Configuration.md) to specify your users, groups and ACL entries. See also the [best practices](docs/BestPractices.md) for hints on structuring. -The property 'migrateFrom' allows to migrate a group name without loosing their members (members of the group given in migrateFrom are taken over and the source=old group deleted afterwards). This property is only to be used temporarily (usually only included in one released version that travels all environments, once all groups are migrated the config should be removed). If not set (the default) nothing happens. If the property points to a group that does not exist (anymore), the property is ignored. +There are also some [advanced configuration options](docs/AdvancedFeatures.md) supported such as loops, conditional statements and permissions for anonymous. -## Configuration of users +# Applying the ACL entries -In general it is best practice to not generate regular users by the AC Tool but use other mechanism (e.g. LDAP) to create users. However, it can be useful to create system users (e.g. for replication agents or OSGi service authentiation) or test users on staging environments. +There are multiple options to [apply the ACL entries](docs/ApplyConfig.md) (e.g. install hook, JMX and upload listener) to your target system. -Users can be configured in the same way as groups in the **user_config** section. The following properties are different to groups (all optional): - -* the attribute "members" cannot be used (for obvious reasons) -* the attribute "password" can be used for preset passwords (not allowed for system users) -* the boolean attribute isSystemUser is used to create system users starting AEM 6.1 -* the attribute profileContent allows to provide docview xml that will reset the profile to the given structure after each run (since v1.8.2) -* the attribute preferencesContent allows to provide docview xml that will reset the preferences node to the given structure after each run (since v1.8.2) - -## Configuration of ACEs - -The configurations are done per principal followed by indented informations which comprise of config data which represents the settings per ACE. This data includes - -property | comment | required ---- | --- | --- -path | a node path. Wildcards `*` are possible. e.g. assuming we have the language trees de and en then `/content/*./test` would match: `/content/de/test` and `/content/en/test` (mandatory). If an asterisk is contained then the path has to be written inside single quotes (`'...'`) since this symbol is a functional character in YAML. | yes -permission | the permission (either `allow` or `deny`) | yes -actions | the actions (`read,modify,create,delete,acl_read,acl_edit,replicate`). Reference: | either actions or privileges need to be present; also a mix of both is possible -privileges | the privileges (`jcr:read, rep:write, jcr:all, crx:replicate, jcr:addChildNodes, jcr:lifecycleManagement, jcr:lockManagement, jcr:modifyAccessControl, jcr:modifyProperties, jcr:namespaceManagement, jcr:nodeTypeDefinitionManagement, jcr:nodeTypeManagement, jcr:readAccessControl, jcr:removeChildNodes, jcr:removeNode, jcr:retentionManagement, jcr:versionManagement, jcr:workspaceManagement, jcr:write, rep:privilegeManagement`). References: | either actions or privileges need to be present; also a mix of both is possible -repGlob |A repGlob expression like "/jcr:*". Please note that repGlobs do not play well together with actions. Use privileges instead (e.g. "jcr:read" instead of read action). See [issue #48](https://github.com/Netcentric/accesscontroltool/issues/48) | no -initialContent | Allows to specify docview xml to create the path if it does not exist. The namespaces for jcr, sling and cq are added automatically if not provided to keep xml short. Initial content must only be specified exactly once per path (this is validated). If paths without permissions should be created, it is possible to provide only a path/initialContent tuple. Available form version 1.7.0. | no - -Every new data entry starts with a "-". - - -Overall format - -``` -[principal] - - path: a valid node path in CRX - permission: [allow/deny] - actions: actions string - privileges: privileges string - repGlob: regex (optional, path restriction as regular expression) - initialContent: (optional) -``` - -Only ACEs for groups which are defined in the same configuration file can be installed! This ensures a consistency between the groups and their ACE definitions per configuration file. - -Cq actions and jcr: privileges can be mixed. If jcr: privileges are already covered by cq actions within an ACE definition they get ignored. Also aggregated privileges like jcr:all or rep:write can be used. - -Example: - -``` -fragment-allow: - - - path: /content/isp - permission: allow - actions: read,modify,create,delete,acl_read,acl_edit,replicate - repGlob: */jcr:content* - - - path: '/content/isp/*/articles' - permission: allow - actions: read,write - privileges: - - - path: /content/intranet - permission: allow - actions: read,write - privileges: crx:replicate - - fragment-deny: - - - path: / - permission: deny - actions: modify,create,delete,acl_read,acl_edit,replicate -``` +# JMX interface -In case the configuration file contains ACEs for groups which are not present in the current configuration no installation takes place and an appropriate error message gets displayed in the history log. +The [JMX interface](docs/Jmx.md) provides utility functions such as installing and dumping ACLs or showing the history. -All important steps performed by the service as well as all error/warning messages get written to error log and history. +# History service -## Loops +A history object collects messages, warnings, and also an exception in case something goes wrong. This history gets saved in CRX under /var/statistics/achistory. The number of histories to be saved can be configured in the history service. -Configuration sections for groups and ACEs allow to use loops to specify multiple, similar entries. In order to do this, a FOR statement has to be used in place of a group name. The FOR statement names a loop variable and lists the values to iterate over. All the children of the FOR element are repeated once per iteration and all group names and property values of child elements that contain the name of the loop variable within '${' and '}' have that expression substituted with the current value of the loop variable. +# Building the packages from source -For example, the following configuration element: - -``` -- FOR brand IN [ BRAND1, BRAND2, BRAND3 ]: - - - content-${brand}-reader: - - - name: - isMemberOf: - path: /home/groups/${brand} -``` - -Gets replaced with - -``` - - content-BRAND1-reader: - - - name: - isMemberOf: - path: /home/groups/BRAND1 - - - content-BRAND2-reader: - - - name: - isMemberOf: - path: /home/groups/BRAND2 - - - content-BRAND3-reader: - - - name: - isMemberOf: - path: /home/groups/BRAND3 -``` - -### Nested Loops - -FOR loops can be nested to any level: - -``` -- for brand IN [ BRAND1, BRAND2 ]: - - - content-${brand}-reader: - - - name: - isMemberOf: - path: /home/groups/${brand} - - - content-${brand}-writer: - - - name: - isMemberOf: - path: /home/groups/${brand} - - - for mkt in [ MKT1, MKT2 ]: - - - content-${brand}-${mkt}-reader: - - - name: - isMemberOf: - path: /home/groups/${brand}/${mkt} - - - content-${brand}-${mkt}-writer: - - - name: - isMemberOf: - path: /home/groups/${brand}/${mkt} -``` - -This will create 12 groups: - -* content-BRAND1-reader -* content-BRAND1-writer -* content-BRAND1-MKT1-reader -* content-BRAND1-MKT1-writer -* content-BRAND1-MKT2-reader -* content-BRAND1-MKT2-writer -* content-BRAND2-reader -* content-BRAND2-writer -* content-BRAND2-MKT1-reader -* content-BRAND2-MKT1-writer -* content-BRAND2-MKT2-reader -* content-BRAND2-MKT2-writer - -### Loops derived from content structure (since 1.8.x) - -For some use cases it is useful to dynamically derive the list of possible values from the content structure. FOR ... IN CHILDREN OF will loop over the children of the provided path (skipping 'jcr:content' nodes) and provide an object with the properties name, path, primaryType, jcr:content (a map of all properties of the respective node) and title (./jcr:content/jcr:title added to root map for convenience). - -``` -- FOR site IN CHILDREN OF /content/myPrj: +If needed you can [build the ACTool yourself](docs/BuildPackage.md). - - content-reader-${site.name}: - - name: Content Reader ${site.title} - isMemberOf: - path: /home/groups/${site.name} -``` - - -### Conditional entries (since 1.8.x) - -When looping over content structures, entries can be applied conditionally using the "IF" keyword: - -``` -- FOR site IN CHILDREN OF /content/myPrj: - - - content-reader-${site.name}: - - name: Content Reader ${site.title} - isMemberOf: - path: /home/groups/${site.name} - - IF ${endsWith(site.name,'-master')}: - - content-reader-master-${site.name}: - - name: Master Content Reader ${site.title} - isMemberOf: - path: /home/groups/global -``` - -Expressions are evaluated using javax.el expression language. The following utility functions are made available to any EL expression used in yaml: - -- split(str,separator) -- join(array,separator) -- subarray(array,startIndexInclusive,endIndexExclusive) -- upperCase(str) -- lowerCase(str) -- substringAfter(str,separator) -- substringBefore(str,separator) -- substringAfterLast(str,separator) -- substringBeforeLast(str,separator) -- contains(str,fragmentStr) -- endsWith(str,fragmentStr) -- startsWith(str,fragmentStr) - -## Configure permissions for anonymous (since 1.8.2) - -Normally it is ensured by validation that a configuration's group system is self-contained - this means out-of-the-box groups like ```contributor``` cannot be used. For registered users in the system this approach works well since either the users are manually assigned to groups (by a user admin) or the membership relationship is maintained by LDAP or SSO extensions. For the ```anonymous``` user on publish that is not logged in by definition, there is no hook that allows to assign it to a group in the AC Tools configuration. Therefore as an exception, it is allowed to use the user ```anonymous``` in the ```members``` attribute of a group configuration. - - -## Validation - -First the validation of the different configuration lines is performed based on regular expressions and gets applied while reading the file. Further validation consists of checking paths for existence as well as for double entries, checks for conflicting ACEs (e.g. allow and deny for same actions on same node), checks whether principals are existing under home. If an invalid parameter or aforementioned issue gets detected, the reading gets aborted and an appropriate error message gets append in the installation history and log. - -If issues occur during the application of the configurations in CRX the installation has to be aborted and the previous state has to stay untouched! Therefore the session used for the installation only gets saved if no exceptions occured thus persisting the changes. - -# Deploying ACLs - -## Storage of configurations in CRX - -Example showing 3 separate project-specific configuration sub-nodes each containing one or more configuration files: - - - -The project specific configuration files are stored in CRX under a node which can be set in the OSGi configuration of the AcService (system/console/configMgr). Each folder underneath this location may contain `*.yaml` files that contain AC configuration. The folder structure gets created by deployment or manually in CRX. - -In general the parent node may specify required Sling run modes being separated by a dot (```.```). Folder names can contain runmodes in the same way as OSGi configurations ([installation of OSGi bundles through JCR packages in Sling](http://sling.apache.org/documentation/bundles/jcr-installer-provider.html)) using a `.` (e.g. `myproject.author` will only become active on author). Additionally, multiple runmodes combinations can be given separated by comma to avoid duplication of configuration (e.g. `myproject.author.test,author.dev` will be active on authors of dev and test environment only). Each time a new configuration file gets uploaded in CRX (e.g. deployment) or the content of a file gets changed a node listener can trigger a new installation of the configurations. This behaviour can be enabled/disabled in UploadListenerService OSGi config. - -## Installation process - -During the installation all groups defined in the groups section of the configuration file get created in the system. In a next step all ACEs configured in the ACE section get installed in CRX. Any old ACEs regarding these groups not contained anymore in the config but in the repository gets automatically purged thus no old or obsolete ACEs stay in the system and any manual change of ACEs regarding these groups get reverted thus ensuring a defined state again. ACEs not belongig to those groups in the configuration files remain untouched. So after the installation took place all groups and the corresponding ACEs exactly as defined in the configuration(s) are installed on the system. - -If at any point during the installation an exception occurs, no changes get persisted in the system. This prevents ending up having a undefined state in the repository. - -During the installation a history containing the most important events gets created and persisted in CRX for later examination. - -The following steps are performed: - -1. All AC entries are removed from the repository which refer to an authorizable being mentioned in the YAML configuration file (no matter to which path those entries refer). -1. All authorizables being mentioned in the YAML configuration get created (if necessary, i.e. if they do no exist yet). -1. All AC entries generated from the YAML configuration get persisted in the repository. If there are already existing entries for one path (and referring to another authorizable) those are not touched. New AC entries are added at the end of the list. All new AC entries are sorted, so that the Deny entries are listed above the Allow entries. Since the AC entry nodes are evaluated bottom-to-top this sorting order leads to less restrictions (e.g. for a user which is member of two groups where one group sets a Deny and the other one sets an Allow, this order ensures that the allow has a higher priority). - - -### Installation Hook - -To automatically install ACEs and Authorizable being defined in YAML files with the package containing the YAML files one can leverage the Content Package Install Hook mechanism. -To enable that on a package being created with Maven through the content-package-maven-plugin one can enable the installation hook via - -``` - - com.day.jcr.vault - content-package-maven-plugin - - - biz.netcentric.cq.tools.actool.installhook.AcToolInstallHook - - - -``` - -The ```*.yaml``` files are installed directly from the package content and respect the same runmode semantics as described above. - -Although it is not necessary that the YAML files are covered by the filter rules of the ```filter.xml```, this is recommended practice. That way you can see afterwards in the repository which YAML files have been processed. However if you would not let the ```filter.xml``` cover your YAML files, those files would still be processed by the installation hook. - -## AC Service - -The main operation purpose of the AC service is the installation of ACE / group definitions from one or several configuration files to a CQ instance on the one hand or the creation of such files (dump) out of an existing configuration on the other hand. It offers possibilities like purging existing ACEs / principals from the instance before installing new ones, merging / adding new ACEs or performing a rollback to a previously saved state if needed. - -The configuration for ACEs principals and groups can be maintained in one file or in dedicated files. - -The configurations are contained in textual files.These files get transferred to CRX usually via deployment. The service can be triggered by a JCR listener which reacts on node changes underneath a configurable path to detect a new configuration in the system, or by triggering the executing it via JMX. - -Before installing a new configuration on an instance a validation of the data stored in the configuration file takes place. In case an issue gets detected the installation doesn't get performed. - - - -## Dump service - -The dump Service is responsible for creating dumps which are accessible via JMX whiteboard. There are 2 kinds of dumps supported: path ordered- and principal ordered dumps. - -* path ordered dumps: here all ACEs in the dump are grouped by path thus representing a complete ACL. This kind of dump gets triggered by the method: pathBasedDump(). -* group based dumps: here all ACEs in the dump are grouped by their respective principal (group or user). This kind of dump gets triggered by the method: groupBasedDump(). - - - -Every created dump can be watched directly in JMX and also gets saved in CRX under /var/statistics/achistory/dump_[Timestamp]. The number of dumps to be saved in CRX can be configured in the OSGi configuration of the dump service in the field: "Number of dumps to save" (see Screenshot) - -There are also 3 additional options available in the OSGi configuration of the dump service: - -* Include user in ACEs in dumps: if checked, all users which have ACEs directly set in the repository get added to the dump (ACEs in section "- ace_config:" and users in section "- user_config:") -* filtered dump: if checked, all ACEs belonging to the cq actions: modifiy, create or delete which have a repGlob set don't get added to the dump. Per default they are omitted since they're automatically set within CQ if one of the action gets set and are not necessary to expicitly being set in a new configuration file. -* include legacy ACEs in dump: if checked, legacy ACEs (ACEs of groups/users which can not be found under /home) also get added to the dump in an extra section called "legacy_aces" at the end of the dump. The order of ACEs listed there is the same as it is for the "valid" ACEs (path based or group based). If needed these ACEs can be manually deleted using the "purge_Authorizables" function by entering the respective principal name(s). - -Internal dumps which get created and used everytime an new AC installation takes place get also created by this service and contain always all ACEs (users and unfilteredACEs). - -## AC (search) Query - -In order to exclude certain direct child nodes of the jcr:root node from the query (e.g. /home, since we don't want to have the rep: policy nodes of each user) to get all the rep: policy nodes in the system, these nodes can be configured. This configuration takes place in the OSGi configuration of the AcService ('/home', '/jcr:system' and '/tmp' are exclude by default). When a query is performed (e.g. during a new installation) , the results of the single queries performed on the nodes which are not excluded get assembled and returned as collective result for further processing. - -## UploadListenerService - -The Upload Listener Service allows to configure the NodeEvent Listener. In the OSGi console it can be enabled/disabled furthermore the listener path can also be configured here. - -If enabled each new upload of a projectspecific configuration file triggers the AceService. Before a new installation starts, a dump (groups & ACLs) gets created which get stored under the backups-node. - -## JMX interface - -The JMX interface offers the possibility to trigger the functions offered by the ACE service. These are: - -* starting a new installation of the newest configuration files in CRX. -* purging of ACLs (of a single node or recursively for all subnodes) -* deletion of single ACEs -* purging users/groups from the instance (including all related ACEs). -* creation of dumps (ordered by path or by group) -* showing of history logs created during the installation - -Also important status messages are shown here: - -* readiness for installation (if at least one configuration file is stored in CRX) -* success status of the last installation -* display of the newest installation files (incl. date information) -* display of the paths of last 5 history logs saved in CTX and a success status of each of those installation - -## History Service - -A history object collects messages, warnings, and also an exception in case something goes wrong. This history gets saved in CRX nder /var/statistics/achistory. The number of histories to be saved can be configured in the history service. - -## Purge ACLs/Authorizables - -Beside the options to install access control configurations and to create dumps the ac tool also offers different possibilities to purge ACLs/authorizables from the system. These methods are also available via JMX whiteboard (see screenshot: JMX Whiteboard of the AC tool). - -Method | Action ---- | --- -purgeACL | This method purges the access control list of a (single) node in repository. The node path is entered as parameter before invocation. -purgeACLs | This method purges the access control list a node and also the access control lists of all child nodes. The node path is entered as parameter before invocation. -purgeAuthorizables | This method purges authorizables from home and also deletes all corresponding ACEs from the repository. Several authorizables are entered as comma separated list before invocation. -purgeAllAuthorizablesFromConfigurations | This method purges all authorizables defined in all configurations files and all their corresponding ACEs from the repository. - -For any of these purge actions a separate purge history (node) containing all logging statements gets persisted in CRX in order to be able to track every of those actions afterwards. Such a purge history node gets saved under the history node of the current ac installation in place. Any of these purge nodes has a timestamp as suffix in the node name. - -## Curl calls - -### Trigger the 'execute' method of the AC service - -``` -curl -sS --retry 1 -u ${CQ_ADMINUSER}:${CQ_ADMINPW} -X POST "http://${CQ_SERVER}:${CQ_PORT}/system/console/jmx/biz.netcentric.cq.tools.actool:id='ac+installation'/op/execute/" -``` +# License +The ACTool is licensed under the [Eclipse Public License - v 1.0](LICENSE.txt). \ No newline at end of file diff --git a/accesscontroltool-bundle/pom.xml b/accesscontroltool-bundle/pom.xml index 7f19b3dc..ffed059f 100644 --- a/accesscontroltool-bundle/pom.xml +++ b/accesscontroltool-bundle/pom.xml @@ -11,7 +11,7 @@ biz.netcentric.cq.tools.accesscontroltool accesscontroltool - 1.8.3 + 1.8.4 diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/AceService.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/AceService.java index 79849d4d..e9b1a4af 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/AceService.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/AceService.java @@ -60,6 +60,13 @@ public interface AceService { public String purgeAuthorizablesFromConfig(); + /** Common entry point for JMX and install hook. + * + * @param session + * @param history + * @param newestConfigurations + * @param authorizableInstallationHistorySet + * @throws Exception */ public void installNewConfigurations(Session session, AcInstallationHistoryPojo history, Map newestConfigurations, Set authorizableInstallationHistorySet) diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/impl/AceServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/impl/AceServiceImpl.java index 99739772..b35ab0bb 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/impl/AceServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/aceservice/impl/AceServiceImpl.java @@ -8,6 +8,8 @@ */ package biz.netcentric.cq.tools.actool.aceservice.impl; +import static biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo.msHumanReadable; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -176,7 +178,7 @@ private void removeAcesForAuthorizables(AcInstallationHistoryPojo history, Sessi String message = "deleted all ACEs of authorizable " + existingAce.getPrincipalName() + " from ACL of path: " + existingAce.getJcrPath(); - LOG.info(message); + LOG.debug(message); history.addVerboseMessage(message); } } @@ -192,7 +194,9 @@ private void installAces( .getPathBasedAceMap(aceMapFromConfig, AcHelper.ACE_ORDER_DENY_ALLOW); - LOG.info("--- start installation of access control configuration ---"); + String msg = "*** Starting installation of "+aceMapFromConfig.size()+" ACLs in content nodes..."; + LOG.info(msg); + history.addMessage(msg); AcHelper.installPathBasedACEs(pathBasedAceMapFromConfig, session, history); } @@ -203,7 +207,9 @@ private void installAuthorizables( throws RepositoryException, Exception { // --- installation of Authorizables from configuration --- - LOG.info("--- start installation of Authorizable Configuration ---"); + String msg = "*** Starting installation of "+authorizablesMapfromConfig.size()+" authorizables..."; + LOG.info(msg); + history.addMessage(msg); // create own session for installation of authorizables since these have // to be persisted in order @@ -235,17 +241,23 @@ private void installAuthorizables( } } - String message = "Finished installation of groups configuration without errors"; + String message = "Finished installation of authorizables without errors"; history.addMessage(message); LOG.info(message); } - /** executes the installation of the existing configurations */ + /** Executes the installation of the existing configurations - entry point for JMX execute() method. */ @Override public AcInstallationHistoryPojo execute() { - Session session = null; AcInstallationHistoryPojo history = new AcInstallationHistoryPojo(); + if (isExecuting) { + history.addError("AC Tool is already executing."); + return history; + } + + Session session = null; + Set authorizableInstallationHistorySet = new LinkedHashSet(); try { @@ -253,7 +265,6 @@ public AcInstallationHistoryPojo execute() { String rootPath = getConfigurationRootPath(); Node rootNode = session.getNode(rootPath); Map newestConfigurations = configFilesRetriever.getConfigFileContentFromNode(rootNode); - installNewConfigurations(session, history, newestConfigurations, authorizableInstallationHistorySet); } catch (AuthorizableCreatorException e) { history.addError(e.toString()); @@ -282,42 +293,59 @@ public AcInstallationHistoryPojo execute() { } } finally { session.logout(); - isExecuting = false; - acHistoryService.persistHistory(history, configurationPath); - } return history; } + /** Common entry point for JMX and install hook. */ @Override - public void installNewConfigurations(Session session, - AcInstallationHistoryPojo history, - Map newestConfigurations, Set authorizableInstallationHistorySet) + public void installNewConfigurations(Session session, AcInstallationHistoryPojo history, Map currentConfiguration, + Set authorizableInstallationHistorySet) throws Exception { - StopWatch sw = new StopWatch(); - sw.start(); - isExecuting = true; + String origThreadName = Thread.currentThread().getName(); + try { + Thread.currentThread().setName(origThreadName + "-ACTool-Config-Worker"); + StopWatch sw = new StopWatch(); + sw.start(); + isExecuting = true; + String message = "*** Applying AC Tool Configuration..."; + LOG.info(message); + history.addMessage(message); + + if (currentConfiguration != null) { - if (newestConfigurations != null) { + history.setConfigFileContentsByName(currentConfiguration); - List mergedConfigurations = configurationMerger.getMergedConfigurations(newestConfigurations, history, configReader); + List mergedConfigurations = configurationMerger.getMergedConfigurations(currentConfiguration, history, configReader); - installMergedConfigurations(history, session, - authorizableInstallationHistorySet, - mergedConfigurations); + installMergedConfigurations(history, session, authorizableInstallationHistorySet, mergedConfigurations); - // if everything went fine (no exceptions), save the session - // thus persisting the changed ACLs - history.addMessage("finished (transient) installation of access control configuration without errors!"); - session.save(); - history.addMessage("persisted changes of ACLs"); + // if everything went fine (no exceptions), save the session + // thus persisting the changed ACLs + history.addVerboseMessage( + "Finished (transient) installation of access control configuration without errors, saving now..."); + session.save(); + history.addMessage("Persisted changes of ACLs"); + } + sw.stop(); + long executionTime = sw.getTime(); + LOG.info("Successfully applied AC Tool configuration in "+ msHumanReadable(executionTime)); + history.setExecutionTime(executionTime); + } catch (Exception e) { + history.addError(e.toString()); // ensure exception is added to history before it's persisted in log in finally clause + throw e; // handling is different depending on JMX or install hook case + } finally { + try { + acHistoryService.persistHistory(history); + } catch (Exception e) { + LOG.warn("Could not persist history, e=" + e, e); + } + + Thread.currentThread().setName(origThreadName); + isExecuting = false; } - sw.stop(); - long executionTime = sw.getTime(); - LOG.info("installation of AccessControlConfiguration took: {} ms", - executionTime); - history.setExecutionTime(executionTime); + } private void installMergedConfigurations( @@ -326,12 +354,13 @@ private void installMergedConfigurations( Set authorizableInstallationHistorySet, List mergedConfigurations) throws ValueFormatException, RepositoryException, Exception { - String message = "start installation of merged configurations"; - LOG.info(message); - history.addMessage(message); + + String message = "Starting installation of merged configurations..."; + LOG.debug(message); + history.addVerboseMessage(message); Map> repositoryDumpAceMap = null; - LOG.info("start building dump from repository"); + LOG.debug("Building dump from repository (to compare delta with config to be installed)"); repositoryDumpAceMap = dumpservice.createAclDumpMap( session, AcHelper.PATH_BASED_ORDER, AcHelper.ACE_ORDER_NONE, @@ -340,6 +369,7 @@ private void installMergedConfigurations( installConfigurationFromYamlList(mergedConfigurations, history, session, authorizableInstallationHistorySet, repositoryDumpAceMap); + } @Override diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableutils/impl/AuthorizableCreatorServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableutils/impl/AuthorizableCreatorServiceImpl.java index c06e4e18..0213ae68 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableutils/impl/AuthorizableCreatorServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableutils/impl/AuthorizableCreatorServiceImpl.java @@ -58,8 +58,7 @@ public class AuthorizableCreatorServiceImpl implements private static final String PATH_HOME_GROUPS = "/home/groups"; private static final String PATH_HOME_USERS = "/home/users"; - private static final Logger LOG = LoggerFactory - .getLogger(AuthorizableCreatorServiceImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(AuthorizableCreatorServiceImpl.class); AcInstallationHistoryPojo status; Map> principalMapFromConfig; @@ -111,7 +110,7 @@ private void installAuthorizableConfigurationBean(final Session session, AuthorizableExistsException, AuthorizableCreatorException { String principalId = authorizableConfigBean.getPrincipalID(); - LOG.info("- start installation of authorizable: {}", principalId); + LOG.debug("- start installation of authorizable: {}", principalId); UserManager userManager = getUsermanager(session); ValueFactory vf = session.getValueFactory(); @@ -144,12 +143,12 @@ private void installAuthorizableConfigurationBean(final Session session, if (authorizableConfigBean.isGroup()) { // this has to be added explicitly here (all other memberships are maintained isMemberOf) - Group groupToInstall = (Group) authorizableToInstall; + Group installedGroup = (Group) userManager.getAuthorizable(principalId); Authorizable anonymous = userManager.getAuthorizable(Constants.USER_ANONYMOUS); if (authorizableConfigBean.membersContainsAnonymous()) { - groupToInstall.addMember(anonymous); + installedGroup.addMember(anonymous); } else { - groupToInstall.removeMember(anonymous); + installedGroup.removeMember(anonymous); } } @@ -286,7 +285,7 @@ private void handleIntermediatePath(final Session session, message.append("recreated authorizable with new intermediate path! " + (newAuthorizable.isGroup() ? "(retained " + countMovedMembersOfGroup + " members of group)" : "")); history.addMessage(message.toString()); - LOG.warn(message.toString()); + LOG.info(message.toString()); } @@ -426,14 +425,14 @@ private void mergeMemberOfGroups(String principalId, Set membershipGroupsFromRepository) throws RepositoryException, AuthorizableExistsException, AuthorizableCreatorException { - LOG.info("...checking differences"); + LOG.debug("...checking differences"); // group in repo doesn't have any members and group in config doesn't // have any members // do nothing if (!isMemberOfOtherGroup(currentGroupFromRepository) && membershipGroupsFromConfig.isEmpty()) { - LOG.info( + LOG.debug( "{}: authorizable in repo is not member of any other group and group in config is not member of any other group. No change necessary here!", principalId); } @@ -469,12 +468,12 @@ else if (isMemberOfOtherGroup(currentGroupFromRepository) private void mergeMemberOfGroupsFromRepo(String principalId, UserManager userManager, Set membershipGroupsFromRepository) throws RepositoryException { - LOG.info( + LOG.debug( "{}: authorizable in repo is member of at least one other group and authorizable in config is not member of any other group", principalId); // delete memberOf groups of that group in repo for (String group : membershipGroupsFromRepository) { - LOG.info( + LOG.debug( "{}: delete authorizable from members of group {} in repository", principalId, group); ((Group) userManager.getAuthorizable(group)) @@ -486,7 +485,7 @@ private void mergeMemberOfGroupsFromConfig(String principalId, AcInstallationHistoryPojo status, UserManager userManager, Set membershipGroupsFromConfig) throws RepositoryException, AuthorizableExistsException, AuthorizableCreatorException { - LOG.info( + LOG.debug( "{}: authorizable in repo is not member of any other group but authorizable in config is member of at least one other group", principalId); @@ -494,7 +493,7 @@ private void mergeMemberOfGroupsFromConfig(String principalId, principalId, membershipGroupsFromConfig.toArray(new String[membershipGroupsFromConfig.size()])); for (Group membershipGroup : validatedGroups) { - LOG.info( + LOG.debug( "{}: add authorizable to members of group {} in repository", principalId, membershipGroup.getID()); @@ -520,11 +519,11 @@ private void mergeMultipleMembersOfBothGroups(String principalId, // are both groups members of exactly the same groups? if (membershipGroupsFromRepository.equals(membershipGroupsFromConfig)) { // do nothing! - LOG.info( + LOG.debug( "{}: authorizable in repo and authorizable in config are members of the same group(s). No change necessary here!", principalId); } else { - LOG.info( + LOG.debug( "{}: authorizable in repo is member of at least one other group and authorizable in config is member of at least one other group", principalId); @@ -540,7 +539,7 @@ private void mergeMultipleMembersOfBothGroups(String principalId, // if not delete that group of membersOf-property of // existing group - LOG.info( + LOG.debug( "delete {} from members of group {} in repository", principalId, authorizable); ((Group) userManager.getAuthorizable(authorizable)) @@ -565,7 +564,7 @@ private void mergeMultipleMembersOfBothGroups(String principalId, // if not add that group to membersOf-property of existing // group - LOG.info("add {} to members of group {} in repository", + LOG.debug("add {} to members of group {} in repository", principalId, validatedGroup); if (StringUtils.equals(validatedGroup.getID(), principalId)) { String warning = "Attempt to add a group as member of itself (" @@ -831,11 +830,17 @@ public void performRollback(SlingRepository repository, history.addWarning("performing Groups rollback!"); for (String authorizableName : newCreatedAuthorizables) { - userManager.getAuthorizable(authorizableName).remove(); - message = "removed authorizable " + authorizableName - + " from the system!"; + Authorizable authorizable = userManager.getAuthorizable(authorizableName); + if (authorizable != null) { + authorizable.remove(); + message = "removed authorizable " + authorizableName + " from the system!"; LOG.info(message); history.addWarning(message); + } else { + message = "Can't remove authorizable " + authorizableName + " from the system!"; + LOG.error(message); + history.addError(message); + } } } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/ConfigFilesRetrieverImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/ConfigFilesRetrieverImpl.java index a9774cf3..986f4ab2 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/ConfigFilesRetrieverImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/ConfigFilesRetrieverImpl.java @@ -68,7 +68,7 @@ private Map getConfigurations(PackageEntryOrNode configFileOrDir } if (isRelevantConfiguration(entry.getName(), configFileOrDir.getName(), currentRunModes)) { - LOG.info("Reading YAML file {}", entry.getName()); + LOG.debug("Found relevant YAML file {}", entry.getName()); configs.put(entry.getPath(), entry.getContentAsString()); } @@ -195,7 +195,7 @@ public String getContentAsString() throws Exception { IOUtils.copy(configInputStream, writer, "UTF-8"); String configData = writer.toString(); if (StringUtils.isNotBlank(configData)) { - LOG.info("found configuration data of node: {}", node.getPath()); + LOG.debug("found configuration data of node: {}", node.getPath()); return configData; } else { throw new IllegalStateException("File " + node.getPath() + " is empty!"); @@ -248,7 +248,7 @@ public boolean isDirectory() throws Exception { @Override public String getContentAsString() throws Exception { - LOG.info("Reading YAML file {}", getPath()); + LOG.debug("Reading YAML file {}", getPath()); InputStream input = archive.getInputSource(entry).getByteStream(); if (input == null) { throw new IllegalStateException("Could not get input stream from entry " + getPath()); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java index 41ae537a..7b69a190 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigReader.java @@ -40,7 +40,7 @@ import biz.netcentric.cq.tools.actool.validators.exceptions.AcConfigBeanValidationException; @Service -@Component(metatype = true, label = "AC Yaml Config Reader", description = "Service that installs groups & ACEs according to textual configuration files") +@Component(label = "AC Yaml Config Reader", description = "Service that installs groups & ACEs according to textual configuration files") public class YamlConfigReader implements ConfigReader { private static final Logger LOG = LoggerFactory.getLogger(YamlConfigReader.class); @@ -81,7 +81,7 @@ public Map> getAceConfigurationBeans(final Collection ac final List aclList = (List) getConfigSection(Constants.ACE_CONFIGURATION_KEY, aceConfigData); if (aclList == null) { - LOG.error("ACL configuration not found in YAML configuration file"); + LOG.debug("ACL configuration not found in this YAML configuration file"); return null; } @@ -98,7 +98,7 @@ public Map> getGroupConfigurationBeans(final final List authorizableList = (List) getConfigSection(Constants.GROUP_CONFIGURATION_KEY, yamlList); if (authorizableList == null) { - LOG.error("Group configuration not found in YAML configuration file"); + LOG.debug("Group configuration not found in this YAML configuration file"); return null; } @@ -128,7 +128,7 @@ private Collection getConfigSection(final String sectionName, final Collection y private Map> getAuthorizablesMap( List yamlMap, final AuthorizableValidator authorizableValidator, boolean isGroupSection) - throws AcConfigBeanValidationException { + throws AcConfigBeanValidationException { final Set alreadyProcessedGroups = new HashSet(); final Map> principalMap = new LinkedHashMap>(); @@ -140,30 +140,27 @@ private Map> getAuthorizablesMap( final String currentPrincipal = (String) currentMap.keySet().iterator().next(); - if (!alreadyProcessedGroups.add(currentPrincipal)) { - throw new IllegalArgumentException( - "There is more than one group definition for group: " - + currentPrincipal); - } - LOG.info("start reading group configuration"); - LOG.info("Found principal: {} in config", currentPrincipal); + if (!alreadyProcessedGroups.add(currentPrincipal)) { + throw new IllegalArgumentException("There is more than one group definition for group: " + currentPrincipal); + } + LOG.debug("Found principal: {} in config", currentPrincipal); - final LinkedHashSet tmpSet = new LinkedHashSet(); - principalMap.put(currentPrincipal, tmpSet); + final LinkedHashSet tmpSet = new LinkedHashSet(); + principalMap.put(currentPrincipal, tmpSet); - final List> currentPrincipalData = (List>) currentMap.get(currentPrincipal); + final List> currentPrincipalData = (List>) currentMap.get(currentPrincipal); - if ((currentPrincipalData != null) && !currentPrincipalData.isEmpty()) { + if ((currentPrincipalData != null) && !currentPrincipalData.isEmpty()) { - for (final Map currentPrincipalDataMap : currentPrincipalData) { - final AuthorizableConfigBean tmpPrincipalConfigBean = new AuthorizableConfigBean(); - setupAuthorizableBean(tmpPrincipalConfigBean, currentPrincipalDataMap, currentPrincipal, isGroupSection); - if (authorizableValidator != null) { - authorizableValidator.validate(tmpPrincipalConfigBean); - } - principalMap.get(currentPrincipal).add(tmpPrincipalConfigBean); + for (final Map currentPrincipalDataMap : currentPrincipalData) { + final AuthorizableConfigBean tmpPrincipalConfigBean = new AuthorizableConfigBean(); + setupAuthorizableBean(tmpPrincipalConfigBean, currentPrincipalDataMap, currentPrincipal, isGroupSection); + if (authorizableValidator != null) { + authorizableValidator.validate(tmpPrincipalConfigBean); } + principalMap.get(currentPrincipal).add(tmpPrincipalConfigBean); } + } } return principalMap; @@ -189,54 +186,51 @@ private Map> getPreservedOrderdAceMap( } for (final Map>> currentPrincipalAceMap : aceYamlList) { - final String principalName = currentPrincipalAceMap.keySet() - .iterator().next(); + final String principalName = currentPrincipalAceMap.keySet().iterator().next(); + final List> aceDefinitions = currentPrincipalAceMap.get(principalName); - final List> aceDefinitions = currentPrincipalAceMap.get(principalName); + LOG.debug("start reading ACE configuration of authorizable: {}", principalName); - LOG.info("start reading ACE configuration of authorizable: {}", - principalName); + // if current principal is not yet in map, add new key and empty + // set for storing the pricipals ACE beans + if (aceMap.get(principalName) == null) { + final Set tmpSet = new LinkedHashSet(); + aceMap.put(principalName, tmpSet); + } - // if current principal is not yet in map, add new key and empty - // set for storing the pricipals ACE beans - if (aceMap.get(principalName) == null) { - final Set tmpSet = new LinkedHashSet(); - aceMap.put(principalName, tmpSet); - } + if ((aceDefinitions == null) || aceDefinitions.isEmpty()) { + LOG.warn("no ACE definition(s) found for autorizable: {}", + principalName); + continue; + } - if ((aceDefinitions == null) || aceDefinitions.isEmpty()) { - LOG.warn("no ACE definition(s) found for autorizable: {}", - principalName); - continue; - } + // create ACE bean and populate it according to the properties + // in the config - // create ACE bean and populate it according to the properties - // in the config + if (aceBeanValidator != null) { + aceBeanValidator.setCurrentAuthorizableName(principalName); + } + for (final Map currentAceDefinition : aceDefinitions) { + final AceBean newAceBean = new AceBean(); + setupAceBean(principalName, currentAceDefinition, + newAceBean); if (aceBeanValidator != null) { - aceBeanValidator.setCurrentAuthorizableName(principalName); + aceBeanValidator.validate(newAceBean, session.getAccessControlManager()); } - for (final Map currentAceDefinition : aceDefinitions) { - final AceBean newAceBean = new AceBean(); - setupAceBean(principalName, currentAceDefinition, + // --- handle wildcards --- + + if ((newAceBean.getJcrPath() != null) + && newAceBean.getJcrPath().contains("*") + && (null != session)) { + handleWildcards(session, aceMap, principalName, newAceBean); - if (aceBeanValidator != null) { - aceBeanValidator.validate(newAceBean, session.getAccessControlManager()); - } - - // --- handle wildcards --- - - if ((newAceBean.getJcrPath() != null) - && newAceBean.getJcrPath().contains("*") - && (null != session)) { - handleWildcards(session, aceMap, principalName, - newAceBean); - } else { - aceMap.get(principalName).add(newAceBean); - } + } else { + aceMap.get(principalName).add(newAceBean); } + } } return aceMap; @@ -341,7 +335,6 @@ private void setupAuthorizableBean( authorizableConfigBean.setPassword(getMapValueAsString( currentPrincipalDataMap, GROUP_CONFIG_PROPERTY_PASSWORD)); - authorizableConfigBean.setProfileContent(getMapValueAsString( currentPrincipalDataMap, USER_CONFIG_PROFILE_CONTENT)); authorizableConfigBean.setPreferencesContent(getMapValueAsString( diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigurationMerger.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigurationMerger.java index 62ec86eb..fe96733a 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigurationMerger.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlConfigurationMerger.java @@ -67,10 +67,11 @@ public List getMergedConfigurations( final ConfigurationsValidator configurationsValidator = new YamlConfigurationsValidator(); for (final Map.Entry entry : newestConfigurations.entrySet()) { - final String message = "Start merging configuration data from: " + entry.getKey(); configurationsValidator.validateMandatorySectionIdentifiersExistence(entry.getValue(), entry.getKey()); + final String message = "Found configuration " + entry.getKey(); + LOG.info(message); history.addMessage(message); List yamlList = (List) yaml.load(entry.getValue()); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroChildNodeObjectsProviderImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroChildNodeObjectsProviderImpl.java index 7a2caa69..d7342a3f 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroChildNodeObjectsProviderImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/configreader/YamlMacroChildNodeObjectsProviderImpl.java @@ -46,7 +46,7 @@ public class YamlMacroChildNodeObjectsProviderImpl implements YamlMacroChildNode @Override public List getValuesForPath(String pathOfChildrenOfClause, AcInstallationHistoryPojo history) { - LOG.info("FOR Loop: Getting children for " + pathOfChildrenOfClause); + LOG.debug("FOR Loop: Getting children for " + pathOfChildrenOfClause); List results = new ArrayList(); @@ -113,7 +113,9 @@ public List getValuesForPath(String pathOfChildrenOfClause, AcInstallati } } - history.addMessage("Loop for children of " + pathOfChildrenOfClause + " evaluates to " + results.size() + " children"); + String msg = "Loop for children of " + pathOfChildrenOfClause + " evaluates to " + results.size() + " children"; + LOG.debug(msg); + history.addMessage(msg); return results; } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AcHelper.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AcHelper.java index ec7bf0c2..188630d8 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AcHelper.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AcHelper.java @@ -42,11 +42,11 @@ import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; public class AcHelper { + public static final Logger LOG = LoggerFactory.getLogger(AcHelper.class); private AcHelper() { } - public static final Logger LOG = LoggerFactory.getLogger(AcHelper.class); public static int ACE_ORDER_DENY_ALLOW = 1; public static int ACE_ORDER_NONE = 2; @@ -86,10 +86,12 @@ public static void installPathBasedACEs( final AcInstallationHistoryPojo history) throws Exception { Set paths = pathBasedAceMapFromConfig.keySet(); - LOG.info("Paths in merged config = {}", paths); - history.addVerboseMessage("found: " + paths.size() - + " paths in merged config"); + LOG.debug("Paths in merged config = {}", paths); + + String msg = "Found " + paths.size() + " paths in config"; + LOG.debug(msg); + history.addVerboseMessage(msg); // loop through all nodes from config for (String path : paths) { @@ -101,7 +103,9 @@ public static void installPathBasedACEs( boolean pathExits = AccessControlUtils.getModifiableAcl(session.getAccessControlManager(), path) != null; if (!pathExits) { if (!ContentHelper.createInitialContent(session, history, path, aceBeanSetFromConfig)) { - history.addWarning("Skipped installing privileges/actions for non existing path: " + path); + String msgNonExistingPath = "Skipped installing privileges/actions for non existing path: " + path; + LOG.debug(msgNonExistingPath); + history.addMessage(msgNonExistingPath); continue; } } @@ -120,7 +124,7 @@ public static void installPathBasedACEs( String message = "deleted all ACEs of authorizable " + bean.getPrincipalName() + " from ACL of path: " + path; - LOG.info(message); + LOG.debug(message); history.addVerboseMessage(message); } writeAcBeansToRepository(session, history, orderedAceBeanSetFromConfig); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java index 5724221e..6d2d1d1e 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AccessControlUtils.java @@ -10,9 +10,7 @@ import java.security.Principal; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -35,25 +33,20 @@ import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; -import org.apache.jackrabbit.api.security.principal.PrincipalManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; - -import com.day.cq.security.util.CqActions; - /** * This class provides common access control related utilities. * Mostly a copy of org.apache.jackrabbit.commons.AccessControlUtils. */ public class AccessControlUtils { + private static final Logger LOG = LoggerFactory.getLogger(AccessControlUtils.class); + private AccessControlUtils() { } - private static final Logger LOG = LoggerFactory - .getLogger(AccessControlUtils.class); /** * Retrieves the {@link Privilege}s from the specified privilege names. @@ -338,9 +331,7 @@ static JackrabbitAccessControlList getModifiableAcl( try { existing = acMgr.getPolicies(path); } catch (PathNotFoundException e) { - AcHelper.LOG - .warn("No node could be found under: {}. Application of ACL for that node cancelled!", - path); + LOG.debug("No node could be found under: {}. Application of ACL for that node cancelled!", path); } if (existing != null) { for (AccessControlPolicy p : existing) { diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AceBean.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AceBean.java index 51309aa4..c1edde38 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AceBean.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/AceBean.java @@ -32,13 +32,15 @@ import org.apache.commons.lang.StringUtils; import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry; import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.day.cq.security.util.CqActions; import biz.netcentric.cq.tools.actool.dumpservice.AcDumpElement; import biz.netcentric.cq.tools.actool.dumpservice.AcDumpElementVisitor; import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; -import com.day.cq.security.util.CqActions; - /** * * @author jochenkoschorke This class is used to store data of an @@ -49,6 +51,7 @@ * configuration file again on the other hand. */ public class AceBean implements AcDumpElement { + public static final Logger LOG = LoggerFactory.getLogger(AceBean.class); private String jcrPath; private String repGlob; @@ -426,7 +429,9 @@ public void install(final Session session, Principal principal, JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl( acMgr, getJcrPath()); if (acl == null) { - history.addWarning("Skipped installing privileges/actions for non existing path: " + getJcrPath()); + String msg = "Skipped installing privileges/actions for non existing path: " + getJcrPath(); + LOG.debug(msg); + history.addMessage(msg); return; } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/PurgeHelper.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/PurgeHelper.java index 72ca3e50..882720f4 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/PurgeHelper.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/PurgeHelper.java @@ -162,8 +162,8 @@ public static String deleteAcesFromAuthorizables(final Session session, } sw.stop(); long executionTime = sw.getTime(); - message.append("\n\ndeleted: " + aceCounter + " ACEs in repository"); - message.append("\nexecution time: " + executionTime + " ms"); + message.append("\n\nDeleted: " + aceCounter + " ACEs in repository"); + message.append("\nExecution time: " + executionTime + " ms"); return message.toString(); } } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcHistoryService.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcHistoryService.java index 0b75514f..d013ac8b 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcHistoryService.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcHistoryService.java @@ -12,8 +12,7 @@ public interface AcHistoryService { - public void persistHistory(AcInstallationHistoryPojo history, - final String configurationRootPath); + public void persistHistory(AcInstallationHistoryPojo history); public void persistAcePurgeHistory(AcInstallationHistoryPojo history); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcInstallationHistoryPojo.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcInstallationHistoryPojo.java index f8156d7e..d478a216 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcInstallationHistoryPojo.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/AcInstallationHistoryPojo.java @@ -10,9 +10,12 @@ import java.sql.Timestamp; import java.text.DateFormat; +import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -31,7 +34,7 @@ public class AcInstallationHistoryPojo { private Set warnings = new HashSet(); private Set messages = new HashSet(); - private Set exceptions = new HashSet(); + private Set errors = new HashSet(); private Set verboseMessages = new HashSet(); @@ -43,6 +46,11 @@ public class AcInstallationHistoryPojo { private String mergedAndProcessedConfig; + private Map configFileContentsByName; + + // only for install hook case + private String crxPackageName; + private DateFormat timestampFormat = new SimpleDateFormat("HH:mm:ss.SSS"); public enum Rendition { @@ -88,6 +96,22 @@ public void setMergedAndProcessedConfig(String mergedAndProcessedConfig) { this.mergedAndProcessedConfig = mergedAndProcessedConfig; } + public Map getConfigFileContentsByName() { + return configFileContentsByName; + } + + public void setConfigFileContentsByName(Map configFileContentsByName) { + this.configFileContentsByName = configFileContentsByName; + } + + public String getCrxPackageName() { + return crxPackageName; + } + + public void setCrxPackageName(String crxPackageName) { + this.crxPackageName = crxPackageName; + } + public void addWarning(String warning) { if (rendition.equals(Rendition.HTML)) { warnings.add(new HistoryEntry(msgIndex, new Timestamp( @@ -109,12 +133,12 @@ public void addMessage(String message) { public void addError(final String exception) { if (rendition.equals(Rendition.HTML)) { - exceptions.add(new HistoryEntry(msgIndex, new Timestamp( + errors.add(new HistoryEntry(msgIndex, new Timestamp( new Date().getTime()), "" + MSG_IDENTIFIER_EXCEPTION + "" + " " + exception + "")); } else if (rendition.equals(Rendition.TXT)) { - exceptions.add(new HistoryEntry(msgIndex, new Timestamp( + errors.add(new HistoryEntry(msgIndex, new Timestamp( new Date().getTime()), MSG_IDENTIFIER_EXCEPTION + " " + exception)); @@ -133,8 +157,8 @@ public Set getMessages() { return messages; } - public Set getException() { - return exceptions; + public Set getErrors() { + return errors; } public boolean isSuccess() { @@ -154,7 +178,7 @@ public String toString() { sb.append("\n" + getMessageHistory() + "\n"); - sb.append("\n" + "Execution time: " + executionTime + " ms\n"); + sb.append("\n" + "Execution time: " + msHumanReadable(executionTime) + "\n"); if (rendition.equals(Rendition.HTML)) { if (success) { @@ -174,13 +198,13 @@ public String toString() { @SuppressWarnings("unchecked") public String getMessageHistory() { return getMessageString(getMessageSet(warnings, messages, - exceptions)); + errors)); } @SuppressWarnings("unchecked") public String getVerboseMessageHistory() { return getMessageString(getMessageSet(warnings, messages, - verboseMessages, exceptions)); + verboseMessages, errors)); } private Set getMessageSet(Set... sets) { @@ -206,4 +230,31 @@ private String getMessageString(Set messageHistorySet) { return sb.toString(); } + /** Utility method to return any magnitude of milliseconds in a human readable format using the appropriate time unit (ms, sec, min) + * depending on the magnitude of the input. + * + * @param millis milliseconds + * @return a string with a number and a unit */ + public static String msHumanReadable(final long millis) { + + double number = millis; + final String[] units = new String[] { "ms", "sec", "min", "h", "days" }; + final double[] divisors = new double[] { 1000, 60, 60, 24 }; + + int magnitude = 0; + do { + double currentDivisor = divisors[Math.min(magnitude, divisors.length - 1)]; + if (number < currentDivisor) { + break; + } + number /= currentDivisor; + magnitude++; + } while (magnitude < units.length - 1); + NumberFormat format = NumberFormat.getNumberInstance(Locale.UK); + format.setMinimumFractionDigits(0); + format.setMaximumFractionDigits(1); + String result = format.format(number) + units[magnitude]; + return result; + } + } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/AcHistoryServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/AcHistoryServiceImpl.java index ce8dc873..617ee4e8 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/AcHistoryServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/AcHistoryServiceImpl.java @@ -9,6 +9,7 @@ package biz.netcentric.cq.tools.actool.installationhistory.impl; import java.io.ByteArrayInputStream; +import java.io.File; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -17,7 +18,6 @@ import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; -import javax.jcr.Workspace; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.Activate; @@ -32,6 +32,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.day.cq.commons.jcr.JcrConstants; + import biz.netcentric.cq.tools.actool.comparators.TimestampPropertyComparator; import biz.netcentric.cq.tools.actool.installationhistory.AcHistoryService; import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; @@ -62,38 +64,25 @@ public void activate(@SuppressWarnings("rawtypes") final Map properties) } @Override - public void persistHistory(AcInstallationHistoryPojo history, - final String configurationRootPath) { + public void persistHistory(AcInstallationHistoryPojo history) { Session session = null; try { - try { - session = repository.loginAdministrative(null); - Node historyNode = HistoryUtils.persistHistory(session, - history, this.nrOfSavedHistories); - - String mergedAndProcessedConfig = history.getMergedAndProcessedConfig(); - if (StringUtils.isNotBlank(mergedAndProcessedConfig)) { - JcrUtils.putFile(historyNode, "mergedConfig.yaml", "text/yaml", - new ByteArrayInputStream(mergedAndProcessedConfig.getBytes())); - } - session.save(); - if (history.isSuccess()) { - Node configurationRootNode = session - .getNode(configurationRootPath); - if (configurationRootNode != null) { - persistInstalledConfigurations(session.getWorkspace(), historyNode, - configurationRootNode, history); - } else { - String message = "Couldn't find configuration root Node under path: " - + configurationRootPath; - LOG.error(message); - history.addWarning(message); - } - } - } catch (RepositoryException e) { - LOG.error("RepositoryException: ", e); + session = repository.loginAdministrative(null); + Node historyNode = HistoryUtils.persistHistory(session, history, this.nrOfSavedHistories); + + String mergedAndProcessedConfig = history.getMergedAndProcessedConfig(); + if (StringUtils.isNotBlank(mergedAndProcessedConfig)) { + JcrUtils.putFile(historyNode, "mergedConfig.yaml", "text/yaml", + new ByteArrayInputStream(mergedAndProcessedConfig.getBytes())); } + + if (history.isSuccess()) { + persistInstalledConfigurations(session, historyNode, history); + } + session.save(); + } catch (RepositoryException e) { + LOG.error("RepositoryException: ", e); } finally { if (session != null) { session.logout(); @@ -134,8 +123,7 @@ public String getLastInstallationHistory() { try { session = repository.loginAdministrative(null); - Node statisticsRootNode = HistoryUtils - .getAcHistoryRootNode(session); + Node statisticsRootNode = HistoryUtils.getAcHistoryRootNode(session); NodeIterator it = statisticsRootNode.getNodes(); if (it.hasNext()) { @@ -157,23 +145,34 @@ public String getLastInstallationHistory() { return history; } - public void persistInstalledConfigurations(final Workspace workspace, final Node historyNode, - final Node configurationRootNode, AcInstallationHistoryPojo history) { + private void persistInstalledConfigurations(final Session session, final Node historyNode, AcInstallationHistoryPojo history) { try { - workspace.copy(configurationRootNode.getPath(), historyNode.getPath() + "/" + INSTALLED_CONFIGS_NODE_NAME); - } catch (RepositoryException e) { - String message = e.toString(); - history.addError(e.toString()); - LOG.error("Exception: ", e); - } - try { - history.addMessage("saved installed configuration files under : " - + historyNode.getPath() + "/" + INSTALLED_CONFIGS_NODE_NAME); + Map configFileContentsByName = history.getConfigFileContentsByName(); + if (configFileContentsByName == null) { + return; + } + + String commonPrefix = StringUtils.getCommonPrefix(configFileContentsByName.keySet().toArray(new String[configFileContentsByName.size()])); + String crxPackageName = history.getCrxPackageName(); // for install hook case + historyNode.setProperty("installedFrom", StringUtils.defaultString(crxPackageName) + commonPrefix); + + for (String fullConfigFilePath : configFileContentsByName.keySet()) { + File targetPathFile = new File( + INSTALLED_CONFIGS_NODE_NAME + "/" + StringUtils.substringAfter(fullConfigFilePath, commonPrefix)); + Node configFolder = JcrUtils.getOrCreateByPath(historyNode, targetPathFile.getParentFile().getPath(), false, + JcrConstants.NT_FOLDER, JcrConstants.NT_FOLDER, false); + ByteArrayInputStream configFileInputStream = new ByteArrayInputStream( configFileContentsByName.get(fullConfigFilePath).getBytes() ); + JcrUtils.putFile(configFolder, targetPathFile.getName(), "text/yaml", configFileInputStream); + } + + history.addVerboseMessage( + "Saved installed configuration files under : " + historyNode.getPath() + "/" + INSTALLED_CONFIGS_NODE_NAME); } catch (RepositoryException e) { - LOG.error("Exception: ", e); + String message = e.toString(); + history.addError(message); + LOG.error("Exception while saving history node " + historyNode + ": " + message, e); } - } public String showHistory(int n) { @@ -260,7 +259,7 @@ private static Node persistPurgeAceHistory(final Session session, previousPurgeNode.getName()); } - String message = "saved history in node: " + purgeHistoryNode.getPath(); + String message = "Saved history in node: " + purgeHistoryNode.getPath(); history.addMessage(message); LOG.info(message); HistoryUtils.setHistoryNodeProperties(purgeHistoryNode, history); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/HistoryUtils.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/HistoryUtils.java index 41f933fd..a0ad2111 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/HistoryUtils.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installationhistory/impl/HistoryUtils.java @@ -22,6 +22,7 @@ import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.version.VersionException; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +30,7 @@ import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; public class HistoryUtils { + private static final Logger LOG = LoggerFactory.getLogger(HistoryUtils.class); public static final String HISTORY_NODE_NAME_PREFIX = "history_"; public static final String NODETYPE_NT_UNSTRUCTURED = "nt:unstructured"; @@ -42,8 +44,6 @@ public class HistoryUtils { private static final String PROPERTY_SUCCESS = "success"; private static final String PROPERTY_INSTALLATION_DATE = "installationDate"; - private static final Logger LOG = LoggerFactory - .getLogger(HistoryUtils.class); public static Node getAcHistoryRootNode(final Session session) throws RepositoryException { @@ -72,9 +72,14 @@ public static Node persistHistory(final Session session, throws RepositoryException { Node acHistoryRootNode = getAcHistoryRootNode(session); - Node newHistoryNode = safeGetNode(acHistoryRootNode, - HISTORY_NODE_NAME_PREFIX + System.currentTimeMillis(), - NODETYPE_NT_UNSTRUCTURED); + String name = HISTORY_NODE_NAME_PREFIX + System.currentTimeMillis(); + if (StringUtils.isNotBlank(history.getCrxPackageName())) { + name += "_via_" + history.getCrxPackageName(); + } else { + name += "_via_jmx"; + } + + Node newHistoryNode = safeGetNode(acHistoryRootNode, name, NODETYPE_NT_UNSTRUCTURED); String path = newHistoryNode.getPath(); setHistoryNodeProperties(newHistoryNode, history); deleteObsoleteHistoryNodes(acHistoryRootNode, nrOfHistoriesToSave); @@ -85,16 +90,16 @@ public static Node persistHistory(final Session session, previousHistoryNode.getName()); } - String message = "saved history in node: " + path; + String message = "Saved history in node: " + path; history.addMessage(message); - LOG.info(message); + LOG.debug(message); return newHistoryNode; } private static Node safeGetNode(final Node baseNode, final String name, final String typeToCreate) throws RepositoryException { if (!baseNode.hasNode(name)) { - LOG.info("create node: {}", name); + LOG.debug("create node: {}", name); return baseNode.addNode(name, typeToCreate); } else { @@ -148,7 +153,7 @@ private static void deleteObsoleteHistoryNodes( int index = 1; for (Node node : historyChildNodes) { if (index > nrOfHistoriesToSave) { - LOG.info("delete obsolete history node: ", node.getPath()); + LOG.debug("delete obsolete history node: ", node.getPath()); node.remove(); } index++; diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHook.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHook.java index 4c284aa8..4295695b 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHook.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHook.java @@ -1,15 +1,23 @@ +/* + * (C) Copyright 2015 Netcentric AG. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ package biz.netcentric.cq.tools.actool.installhook; import org.osgi.framework.ServiceReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; -import biz.netcentric.cq.tools.actool.installationhistory.HistoryEntry; - import com.day.jcr.vault.packaging.InstallContext; import com.day.jcr.vault.packaging.PackageException; +import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; +import biz.netcentric.cq.tools.actool.installationhistory.HistoryEntry; + public class AcToolInstallHook extends OsgiAwareInstallHook { private static final Logger LOG = LoggerFactory.getLogger(AcToolInstallHook.class); @@ -60,7 +68,7 @@ public void execute(InstallContext context) throws PackageException { } if (!history.isSuccess()) { - for (HistoryEntry entry : history.getException()) { + for (HistoryEntry entry : history.getErrors()) { log(entry.toString(), context.getOptions()); } throw new PackageException( diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookService.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookService.java index fa7a057d..7a7b28d0 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookService.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookService.java @@ -1,11 +1,19 @@ +/* + * (C) Copyright 2015 Netcentric AG. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ package biz.netcentric.cq.tools.actool.installhook; import javax.jcr.Session; -import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; - import com.day.jcr.vault.fs.io.Archive; +import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; + public interface AcToolInstallHookService { public AcInstallationHistoryPojo installYamlFilesFromPackage(Archive archive, Session session) diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookServiceImpl.java index 9713f706..352851a6 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/AcToolInstallHookServiceImpl.java @@ -1,7 +1,16 @@ +/* + * (C) Copyright 2015 Netcentric AG. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ package biz.netcentric.cq.tools.actool.installhook; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Properties; import java.util.Set; import javax.jcr.Session; @@ -12,13 +21,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.day.jcr.vault.fs.io.Archive; + import biz.netcentric.cq.tools.actool.aceservice.AceService; import biz.netcentric.cq.tools.actool.authorizableutils.AuthorizableInstallationHistory; import biz.netcentric.cq.tools.actool.configreader.ConfigFilesRetriever; import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo; -import com.day.jcr.vault.fs.io.Archive; - @Component @Service(value = AcToolInstallHookService.class) public class AcToolInstallHookServiceImpl implements AcToolInstallHookService { @@ -37,16 +46,16 @@ public AcInstallationHistoryPojo installYamlFilesFromPackage(Archive archive, Se AcInstallationHistoryPojo history = new AcInstallationHistoryPojo(); Set authorizableInstallationHistorySet = new LinkedHashSet(); - try { - Map configs = configFilesRetriever.getConfigFileContentFromPackage(archive); - aceService.installNewConfigurations(session, history, configs, authorizableInstallationHistorySet); - } catch (Exception e) { - history.addError(e.toString()); - throw e; - } finally { - // TODO: acHistoryService.persistHistory(history, - // this.configurationPath); - } + Map configs = configFilesRetriever.getConfigFileContentFromPackage(archive); + history.setCrxPackageName(getArchiveName(archive)); + aceService.installNewConfigurations(session, history, configs, authorizableInstallationHistorySet); + return history; } + + private String getArchiveName(Archive archive) { + Properties properties = archive.getMetaInf().getProperties(); + String archiveName = properties != null ? (properties.getProperty("name") + "-" + properties.getProperty("version")) : null; + return archiveName; + } } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/OsgiAwareInstallHook.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/OsgiAwareInstallHook.java index 6baeced5..c2f4a648 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/OsgiAwareInstallHook.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/installhook/OsgiAwareInstallHook.java @@ -1,8 +1,15 @@ +/* + * (C) Copyright 2015 Netcentric AG. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ package biz.netcentric.cq.tools.actool.installhook; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleReference; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceReference; import org.slf4j.Logger; @@ -40,7 +47,7 @@ public ServiceReference getServiceReference(String clazz) { return serviceReference; } - public void log (String message, ImportOptions options) { + public void log(String message, ImportOptions options) { ProgressTrackerListener listener = options.getListener(); if (listener != null) { listener.onMessage(ProgressTrackerListener.Mode.TEXT, message, ""); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/validators/impl/AceBeanValidatorImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/validators/impl/AceBeanValidatorImpl.java index 5cd3a229..1ef683ba 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/validators/impl/AceBeanValidatorImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/validators/impl/AceBeanValidatorImpl.java @@ -290,7 +290,7 @@ private String getBeanDescription(final long beanCounter, @Override public void setCurrentAuthorizableName(final String name) { if (this.enabled) { - LOG.info("start validation of ACEs for authorizable: {}", name); + LOG.debug("Start validation of ACEs for authorizable: {}", name); this.currentBeanCounter = 0; } } diff --git a/accesscontroltool-exampleconfig-package/pom.xml b/accesscontroltool-exampleconfig-package/pom.xml new file mode 100644 index 00000000..abe3e008 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/pom.xml @@ -0,0 +1,119 @@ + + + + 4.0.0 + + + + + biz.netcentric.cq.tools.accesscontroltool + accesscontroltool + 1.8.4 + + + + + + + accesscontroltool-exampleconfig-package + content-package + Access Control Tool Package - Example Configuration + + + + + src/main/jcr_root + false + + **/.vlt + **/.vltignore + + + + src/main/META-INF/vault/definition + ../vault-work/META-INF/vault/definition + + + + + + org.apache.maven.plugins + maven-resources-plugin + + true + + + + + com.day.jcr.vault + content-package-maven-plugin + true + + Netcentric + src/main/META-INF/vault/filter.xml + http://${crx.host}:${crx.port}/crx/packmgr/service.jsp + + + + + + + + + + autoInstallPackage + + + + com.day.jcr.vault + content-package-maven-plugin + + + install-content-package + install + + install + + + + + + + + + + autoInstallPackagePublish + + + + com.day.jcr.vault + content-package-maven-plugin + + + install-content-package-publish + install + + install + + + http://${publish.crx.host}:${publish.crx.port}/crx/packmgr/service.jsp + ${publish.crx.username} + ${publish.crx.password} + + + + + + + + + + + diff --git a/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/config.xml b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/config.xml new file mode 100644 index 00000000..941af6c2 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/config.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/definition/thumbnail.png b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/definition/thumbnail.png new file mode 100644 index 00000000..7eae31b2 Binary files /dev/null and b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/definition/thumbnail.png differ diff --git a/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/filter.xml b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/filter.xml new file mode 100644 index 00000000..22706922 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/filter.xml @@ -0,0 +1,4 @@ + + + + diff --git a/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/settings.xml b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/settings.xml new file mode 100644 index 00000000..68ce8404 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/META-INF/vault/settings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/.gitignore b/accesscontroltool-exampleconfig-package/src/main/jcr_root/.gitignore new file mode 100644 index 00000000..3385916d --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/.gitignore @@ -0,0 +1 @@ +/META-INF/ diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/Readme.md b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/Readme.md new file mode 100644 index 00000000..d5ae50c4 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/Readme.md @@ -0,0 +1,5 @@ +The subfolders include examples for ACTool configurations. + +* [multipleFiles](multipleFiles): multiple config files in one directory +* [runmodes](runmodes): run mode specific configurations +* [simple](simple): just one simple configuration file \ No newline at end of file diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/Readme.md b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/Readme.md new file mode 100644 index 00000000..ea797261 --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/Readme.md @@ -0,0 +1 @@ +Defines two simple groups with a single ACL. \ No newline at end of file diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup.yaml b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup.yaml new file mode 100644 index 00000000..123ac38a --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup.yaml @@ -0,0 +1,17 @@ +- group_config: + + - testgroup-tags: + + - name: Test group for tag management + isMemberOf: + members: + path: t + +- ace_config: + + - testgroup-tags: + + - path: /etc/tags + permission: allow + actions: read + privileges: diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup2.yaml b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup2.yaml new file mode 100644 index 00000000..a98e897f --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/multipleFiles/testgroups/testgroup2.yaml @@ -0,0 +1,17 @@ +- group_config: + + - testgroup2-tags: + + - name: Test group 2 for tag management + isMemberOf: + members: + path: t + +- ace_config: + + - testgroup2-tags: + + - path: /etc/tags + permission: allow + actions: read + privileges: diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/Readme.md b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/Readme.md new file mode 100644 index 00000000..c473560b --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/Readme.md @@ -0,0 +1,3 @@ +Defines a simple group with a single ACL. The config depends on run mode. +* testgroup.author: installed on author only +* testgroup.author.dev,author.test: installed only on author environments that have a second run mode dev or test \ No newline at end of file diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup.author/config.yaml b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup.author/config.yaml new file mode 100644 index 00000000..123ac38a --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup.author/config.yaml @@ -0,0 +1,17 @@ +- group_config: + + - testgroup-tags: + + - name: Test group for tag management + isMemberOf: + members: + path: t + +- ace_config: + + - testgroup-tags: + + - path: /etc/tags + permission: allow + actions: read + privileges: diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup2.author.dev,author.test/config.yaml b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup2.author.dev,author.test/config.yaml new file mode 100644 index 00000000..a98e897f --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/runmodes/testgroup2.author.dev,author.test/config.yaml @@ -0,0 +1,17 @@ +- group_config: + + - testgroup2-tags: + + - name: Test group 2 for tag management + isMemberOf: + members: + path: t + +- ace_config: + + - testgroup2-tags: + + - path: /etc/tags + permission: allow + actions: read + privileges: diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/Readme.md b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/Readme.md new file mode 100644 index 00000000..a199a62a --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/Readme.md @@ -0,0 +1 @@ +Defines a simple group with a single ACL. \ No newline at end of file diff --git a/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/testgroup/config.yaml b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/testgroup/config.yaml new file mode 100644 index 00000000..123ac38a --- /dev/null +++ b/accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig/simple/testgroup/config.yaml @@ -0,0 +1,17 @@ +- group_config: + + - testgroup-tags: + + - name: Test group for tag management + isMemberOf: + members: + path: t + +- ace_config: + + - testgroup-tags: + + - path: /etc/tags + permission: allow + actions: read + privileges: diff --git a/accesscontroltool-oakindex-package/pom.xml b/accesscontroltool-oakindex-package/pom.xml index 0207babb..b45c40a0 100644 --- a/accesscontroltool-oakindex-package/pom.xml +++ b/accesscontroltool-oakindex-package/pom.xml @@ -15,7 +15,7 @@ biz.netcentric.cq.tools.accesscontroltool accesscontroltool - 1.8.3 + 1.8.4 diff --git a/accesscontroltool-package/pom.xml b/accesscontroltool-package/pom.xml index 1d90222a..13e4ad72 100644 --- a/accesscontroltool-package/pom.xml +++ b/accesscontroltool-package/pom.xml @@ -15,7 +15,7 @@ biz.netcentric.cq.tools.accesscontroltool accesscontroltool - 1.8.3 + 1.8.4 diff --git a/docs/AdvancedFeatures.md b/docs/AdvancedFeatures.md new file mode 100644 index 00000000..b96bb2a0 --- /dev/null +++ b/docs/AdvancedFeatures.md @@ -0,0 +1,141 @@ +# Advanced features + +## Loops + +Configuration sections for groups and ACEs allow to use loops to specify multiple, similar entries. In order to do this, a FOR statement has to be used in place of a group name. The FOR statement names a loop variable and lists the values to iterate over. All the children of the FOR element are repeated once per iteration and all group names and property values of child elements that contain the name of the loop variable within '${' and '}' have that expression substituted with the current value of the loop variable. + +For example, the following configuration element: + +``` +- FOR brand IN [ BRAND1, BRAND2, BRAND3 ]: + + - content-${brand}-reader: + + - name: + isMemberOf: + path: /home/groups/${brand} +``` + +Gets replaced with + +``` + - content-BRAND1-reader: + + - name: + isMemberOf: + path: /home/groups/BRAND1 + + - content-BRAND2-reader: + + - name: + isMemberOf: + path: /home/groups/BRAND2 + + - content-BRAND3-reader: + + - name: + isMemberOf: + path: /home/groups/BRAND3 +``` + +### Nested Loops + +FOR loops can be nested to any level: + +``` +- for brand IN [ BRAND1, BRAND2 ]: + + - content-${brand}-reader: + + - name: + isMemberOf: + path: /home/groups/${brand} + + - content-${brand}-writer: + + - name: + isMemberOf: + path: /home/groups/${brand} + + - for mkt in [ MKT1, MKT2 ]: + + - content-${brand}-${mkt}-reader: + + - name: + isMemberOf: + path: /home/groups/${brand}/${mkt} + + - content-${brand}-${mkt}-writer: + + - name: + isMemberOf: + path: /home/groups/${brand}/${mkt} +``` + +This will create 12 groups: + +* content-BRAND1-reader +* content-BRAND1-writer +* content-BRAND1-MKT1-reader +* content-BRAND1-MKT1-writer +* content-BRAND1-MKT2-reader +* content-BRAND1-MKT2-writer +* content-BRAND2-reader +* content-BRAND2-writer +* content-BRAND2-MKT1-reader +* content-BRAND2-MKT1-writer +* content-BRAND2-MKT2-reader +* content-BRAND2-MKT2-writer + +### Loops derived from content structure (since 1.8.x) + +For some use cases it is useful to dynamically derive the list of possible values from the content structure. FOR ... IN CHILDREN OF will loop over the children of the provided path (skipping 'jcr:content' nodes) and provide an object with the properties name, path, primaryType, jcr:content (a map of all properties of the respective node) and title (./jcr:content/jcr:title added to root map for convenience). + +``` +- FOR site IN CHILDREN OF /content/myPrj: + + - content-reader-${site.name}: + - name: Content Reader ${site.title} + isMemberOf: + path: /home/groups/${site.name} +``` + + +### Conditional entries (since 1.8.x) + +When looping over content structures, entries can be applied conditionally using the "IF" keyword: + +``` +- FOR site IN CHILDREN OF /content/myPrj: + + - content-reader-${site.name}: + - name: Content Reader ${site.title} + isMemberOf: + path: /home/groups/${site.name} + + IF ${endsWith(site.name,'-master')}: + - content-reader-master-${site.name}: + - name: Master Content Reader ${site.title} + isMemberOf: + path: /home/groups/global +``` + +Expressions are evaluated using javax.el expression language. The following utility functions are made available to any EL expression used in yaml: + +- split(str,separator) +- join(array,separator) +- subarray(array,startIndexInclusive,endIndexExclusive) +- upperCase(str) +- lowerCase(str) +- substringAfter(str,separator) +- substringBefore(str,separator) +- substringAfterLast(str,separator) +- substringBeforeLast(str,separator) +- contains(str,fragmentStr) +- endsWith(str,fragmentStr) +- startsWith(str,fragmentStr) + +## Configure permissions for anonymous (since 1.8.2) + +Normally it is ensured by validation that a configuration's group system is self-contained - this means out-of-the-box groups like ```contributor``` cannot be used. For registered users in the system this approach works well since either the users are manually assigned to groups (by a user admin) or the membership relationship is maintained by LDAP or SSO extensions. For the ```anonymous``` user on publish that is not logged in by definition, there is no hook that allows to assign it to a group in the AC Tools configuration. Therefore as an exception, it is allowed to use the user ```anonymous``` in the ```members``` attribute of a group configuration. + diff --git a/docs/ApplyConfig.md b/docs/ApplyConfig.md new file mode 100644 index 00000000..932d50aa --- /dev/null +++ b/docs/ApplyConfig.md @@ -0,0 +1,59 @@ +# Applying ACLs + +The following steps are performed: + +1. All AC entries are removed from the repository which refer to an authorizable (user/group) being mentioned in the YAML configuration file (no matter to which path those entries refer). +1. All authorizables being mentioned in the YAML configuration get created (if necessary, i.e. if they do no exist yet). +1. All AC entries generated from the YAML configuration get persisted in the repository. If there are already existing entries for one path (and referring to another authorizable) those are not touched. New AC entries are added at the end of the list. All new AC entries are sorted, so that the Deny entries are listed above the Allow entries. Since the AC entry nodes are evaluated bottom-to-top this sorting order leads to less restrictions (E.g. a user might be member of two groups where one group sets a Deny and the other one sets an Allow. This order ensures that the Allow has a higher priority.). + +If at any point during the installation an exception occurs, no changes get persisted in the system. This prevents ending up having a undefined state in the repository. + +During the installation a history containing the most important events gets created and persisted in CRX for later examination. + + +### Installation Hook + +You can automatically install ACEs and authorizables defined in YAML files within a package using the Content Package Install Hook mechanism. +If you use the content-package-maven-plugin enable the installation hook via: + +``` + + com.day.jcr.vault + content-package-maven-plugin + + + biz.netcentric.cq.tools.actool.installhook.AcToolInstallHook + + + +``` + +The ```*.yaml``` files are installed directly from the package content and respect the [run mode semantics](Configuration.md). + +Although it is not necessary that the YAML files are covered by the filter rules of the ```filter.xml```, this is recommended practice. That way you can see afterwards in the repository which YAML files have been processed. However if you would not let the ```filter.xml``` cover your YAML files, those files would still be processed by the installation hook. + +## JMX + +See [JMX execute() method](Jmx.md). + +## Upload Listener Service + +The Upload Listener Service allows to configure the NodeEvent Listener. + +If enabled each new upload of a project specific configuration file triggers the installation. Before a new installation starts, a dump (groups & ACLs) gets created and stored under the backups-node. + +It can be enabled/disabled in the OSGi console (AC Configuration Upload Listener Service). The listener path is configured in AC Installation Service. + + + + + + +## Curl calls + +Trigger the 'execute' method of the AC service + +``` +curl -sS --retry 1 -u ${CQ_ADMINUSER}:${CQ_ADMINPW} -X POST "http://${CQ_SERVER}:${CQ_PORT}/system/console/jmx/biz.netcentric.cq.tools.actool:id='ac+installation'/op/execute/" +``` + diff --git a/docs/BestPractices.md b/docs/BestPractices.md new file mode 100644 index 00000000..aff3dde5 --- /dev/null +++ b/docs/BestPractices.md @@ -0,0 +1,93 @@ +# Best Practices + +## Split files by project + +In case you have multiple projects/teams you should split the config files for them in case groups and ACLs are independent. E.g. both project's content packages can ship with their own files containing only their groups. This makes it easier to update the rights later. + +In case groups are shared between projects you should create a common package. + +## Split files by topic + +Create multiple folders for e.g. service users, generic role definitions and actual roles that are assigned to users. This will help you to find the place where to make changes. + +## Use one package for all environments + +There is no need to setup multiple content packages to install the ACTool config files in multiple environments. You can use run modes in case there are differences between environments. + +## Create demo users with test content + +Since you can also create users it makes perfect sense to add some demo users in your test content package. This way you always have a set of defined users with group permissions installed. Of course, test content must not be installed on production to have no security problem. + +## Always assign permissions to groups, never to single users + +You have many more users than groups so groups simplify the structure. Groups help provide an overview over all accounts. Inheritance is simpler with groups. Users come and go while groups are existing long-term. + +## Always use Allow statements. Avoid using a Deny statement (whenever possible) + +If a user is a member of more than one group, the deny statements from one group may cancel the allow statement from another group, or vice versa. This can cause unexpected effects. Use a model which introduces basic allow/deny fragments in combination with functional/content fragments. + +## Always test your access rights configuration on a test system + +Before applying a configuration to production it should have been thoroughly tested on a dedicated test instance. In general there should always be a test instance available on which a full production copy of the access configuration is available/testable. Keep the access control configuration of production and test systems in sync. + +## Consider access rights when designing you content structure + +Make access rights one of the main drivers of your information architecture. + +## Keep it simple + +Try to keep the number of groups as small as possible by detecting similarities and using fragments. Try to achieve a clear hierarchy of groups/fragments. + +## Avoid globbing + +Since globbing expressions get implicitly applied to child nodes they are not directly visible in content. Avoid them if possible. + +## Remember that ACL evaluation slows down the system responsiveness + +The longer the list of ACEs get the longer the permission evaluation takes place. Therefore, try to keep the list short. + +## White listing nodes + +There are cases where you want to deny access to a node's children by default and only allow access to specific children. This requirement is quite common for /content. +You can achieve this by using globbing. In the example below /content is denied for all users in fragment-restrict-for-everyone. But this will not only deny access to the child nodes but also to /content itself. So we need two more rules: + +* allow to /content with repGlob "": This will allow access to the /content node itself ("" as repGlob means just this node) +* allow to /content with repGlob "jcr:*": This is needed e.g. for site admin since it needs to read the jcr:primaryType property. + +This will allow to see /content without its children. + +In a second group (fragment-project1) you can then allow to read the child node /content/project1. + + +``` + - fragment-restrict-for-everyone: + + - path: /content + permission: deny + actions: + privileges: jcr:read,jcr:readAccessControl + repGlob: + + - path: /content + permission: allow + actions: + privileges: jcr:read,jcr:readAccessControl + repGlob: "" + + - path: /content + permission: allow + actions: + privileges: jcr:read,jcr:readAccessControl + repGlob: /jcr:* + + - fragment-project1: + + - path: /content/project1 + permission: allow + actions: + privileges: jcr:read,jcr:readAccessControl + repGlob: + +``` + + diff --git a/docs/BuildPackage.md b/docs/BuildPackage.md new file mode 100644 index 00000000..7fd0dafc --- /dev/null +++ b/docs/BuildPackage.md @@ -0,0 +1,32 @@ +# Building the packages from source +## Requirements + +Building the ACTool requires Java 7 and Maven 3.2. + +## Build package + +A full build of ACTool can be executed by running: + +``` +mvn clean install +``` + +This command will create an AEM/CQ Package as a ZIP file inside accesscontroltool-package/target called accesscontroltool-package-.zip. + +The package can be installed using the AEM Package Manager. +If you run AEM on http://localhost:4502 you can also install from the command line using this command: + +``` +mvn -PautoInstallPackage install +``` + +## AEM6.x/Oak + +The `oakindex-package` contains an optimized Oak index to cover all queries being issued by the Access Control Tool. To build (and optionally deploy) the content-package use the Maven profile oakindex. This package is only compatible with Oak and even there it is optional (as it will only speed up queries). + +To use the package, run all commands with profile `oakindex`, e.g. + ``` +mvn clean install -Poakindex + ``` + +Output will be accesscontroltool-oakindex-package/target/accesscontroltool-oakindex-package-.zip \ No newline at end of file diff --git a/docs/Comparison.md b/docs/Comparison.md new file mode 100644 index 00000000..4e553c8e --- /dev/null +++ b/docs/Comparison.md @@ -0,0 +1,19 @@ +# Comparison of AC Tool to other approaches + +We considered existing solutions before starting our own AC Tool. These are basically content packages (including rep:policy nodes) and the ACL Setup Service provided by AEM. + +Aspect | AC Tool | Content Package | ACL Setup Service +------ | ------- | --------------- | ----------------- +Readability | :white_check_mark: config can be read by less technical persons | :x: hard to read even for developers | :large_orange_diamond: readable for small setups +Run mode support | :white_check_mark: | :x: | :x: +Setting ACLs for a content position | :white_check_mark: | :large_orange_diamond: if path does not exist, invalid pages are created | :white_check_mark: +Creation of groups possible | :white_check_mark: | :white_check_mark: | :x: +Order of ACEs is ensured | order of ACEs is ensured | :x: works for initial creation, but not incrementally | :x: works for initial creation, but not incrementally +Old entries can be deleted | :white_check_mark: before applying ACEs to a content node, all entries are removed to ensure the ACL exactly as provided by AC Tool configuration file | :x: old entries are untouched and have to be deleted manually | :x: old entries are untouched and have to be deleted manually +Consistency Checks regarding AC setup | :white_check_mark: | :x: | :x: +Maintainability | :white_check_mark: Single configuration file per project keeps ACL setup in one place. Can be split up to multiple files (e.g. one per tenant). | :x: package with many filter rules and complex structure has to be created | :large_orange_diamond: Everything is kept in one file (OSGi configuration), good for small projects but gets too big for large instances. +Duplication in configuration | :white_check_mark: supports wildcards and loops | :x: all paths have to be contained in package | :x: all paths have to be explicitly listed in OSGi config +Automatic Group Location Migration | :white_check_mark: if the location of a group changes in the config file, the AC Tool automatically migrates the group location and all references to it in the content | :x: all paths in content package have to be changed manually | :x: cannot handle groups +Import/Export | :white_check_mark: import and export of Yaml files | :x: no standard tool in AEM for exporting ACEs but :white_check_mark: [ACL Packager](http://adobe-consulting-services.github.io/acs-aem-commons/features/acl-packager.html) can be used | :x: no export of the effective permissions of an instance +Reproducibility | :white_check_mark: It is possible to ensure that ACL settings in any system are exactly as defined. | :x: Old ACLs are not removed. Therefore, it can only be ensured that the defined ACLs are there but there may be additional ones active as well. | :x: Old ACLs are not removed. Therefore, it can only be ensured that the defined ACLs are there but there may be additional ones active as well. +Availability | :large_orange_diamond: requires installation of additional package | :white_check_mark: part of deployment packages | :white_check_mark: included out-of-the-box \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 00000000..66baba24 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,178 @@ +# Configuration File Format + +For better human readability and easy editing the ACL configuration files use the YAML format. + +You can split your configuration to multiple files and directories. See also [best practices](BestPractices.md). Each folder can include one or more Yaml files ("*.yaml"). +The file format is the same for all files. + +You can find some examples in [example config package](../accesscontroltool-exampleconfig-package/src/main/jcr_root/apps/netcentric/actool-exampleconfig). + + +## Storage of configurations in CRX + +This example shows three separate project specific configuration subnodes (multipleFiles, runmodes, simple) each containing one or more configuration files: + + + +The project specific configuration files are stored in CRX under a node which can be set in the OSGi configuration of the AcService (system/console/configMgr). Each folder underneath this location may contain `*.yaml` files that contain AC configuration. You can use a normal content package to deploy the files. + +## Run modes + +In general the parent node may specify required Sling run modes being separated by a dot (```.```). Folder names can contain runmodes in the same way as OSGi configurations ([installation of OSGi bundles through JCR packages in Sling](http://sling.apache.org/documentation/bundles/jcr-installer-provider.html)) using a `.` (e.g. `myproject.author` will only become active on author). Additionally, multiple runmodes combinations can be given separated by comma to avoid duplication of configuration (e.g. `myproject.author.test,author.dev` will be active on authors of dev and test environment only). + +Examples: + +* project.author: runs on "author" run mode only +* project.author.dev: runs only when run modes "author" and "dev" are present +* project.author.test,author.dev: requires run mode "author" and either "test" or "dev" to be present + +## Overall structure a of an AC configuration file + + + +Every configuration file comprises a group section where groups and their membership to other groups get defined and a ACE section where all ACEs in the repository regarding these groups get defined. These ACE definitions are again written under the respective group id. The group of an ACE definition in each configuration file has to match a group which is also defined in the same file. Groups which are contained in the isMemberOf property within a group definition have either to be defined in another configuration file or already be installed in the system on which the installation takes place. + +## Configuration of groups + +Groups are specified in the **group_config** A group record in the configuration file starts with the principal id followed by some indented data records containing properties like name/description and group membership: + +``` +[Groupd Id] + - name: group name (optional, if empty group id is taken) + - isMemberOf: comma separated list of other groups (optional) + - members: comma separated list of groups that are member of this group + - description: (optional, description) + - path: (optional, path of the group in JCR) + - migrateFrom: (optional, a group name assigned member users are taken over from, since v1.7) +``` + +Example: + +``` +- group_config: + + - myeditors: + - isMemberOf: mystaff + members: editor + + - system-read: + - isMemberOf: + description: system users with read access + members: system-reader +``` + +If the isMemberOf property of a group contains a group which is not yet installed in the repository, this group gets created and its rep:members property gets filled accordingly. If another configuration gets installed having a actual definition for that group the data gets merged into the already existing one. + +The members property contains a list of groups where this group is added as isMemberOf. + +### Group migration + +The property 'migrateFrom' allows to migrate a group name without loosing their members. Members of the group given in migrateFrom are taken over and the source/old group is deleted afterwards. This property is only to be used temporarily. Usually, it is only included in one released version that travels all environments. Once all groups are migrated the migrateFrom property should be removed. If the property points to a group that does not exist (anymore) the property is ignored. + +## Configuration of users + +In general it is best practice to not generate regular users by the AC Tool but use other mechanism (e.g. LDAP) to create users. However, it can be useful to create system users (e.g. for replication agents or OSGi service authentiation) or test users on staging environments. + +Users can be configured in the same way as groups in the **user_config** section. + +``` +[User Id] + - name: user name (optional, if empty user id is taken) + - isMemberOf: comma separated list of groups (optional) + - description: (optional, description) + - path: (optional, path of the group in JCR) + - isSystemUser: the created user is a system user (AEM 6.1 and later) (default: false, optional) + - password: can be used to preset passwords (not allowed for system users) (optional) + - profileContent: allows to provide docview xml that will reset the profile to the given structure after each run (optional, since v1.8.2) + - preferencesContent: allows to provide docview xml that will reset the preferences node to the given structure after each run (optional, since v1.8.2) +``` + +Example: + +``` +- user_config: + + - editor: + - isMemberOf: myeditors + password: secret + + - system-reader: + - name: system-reader + isMemberOf: system-read + path: s + isSystemUser: true +``` + +Group memberships can be set on user entry or group entry or both. + +## Configuration of ACEs + +The configurations are done per principal followed by indented settings for each ACE. This data includes + +property | comment | required +--- | --- | --- +path | a node path. Wildcards `*` are possible. e.g. assuming we have the language trees de and en then `/content/*./test` would match: `/content/de/test` and `/content/en/test` (mandatory). If an asterisk is contained then the path has to be written inside single quotes (`'...'`) since this symbol is a functional character in YAML. | yes +permission | the permission (either `allow` or `deny`) | yes +actions | the actions (`read,modify,create,delete,acl_read,acl_edit,replicate`). Reference: | either actions or privileges need to be present; also a mix of both is possible +privileges | the privileges (`jcr:read, rep:write, jcr:all, crx:replicate, jcr:addChildNodes, jcr:lifecycleManagement, jcr:lockManagement, jcr:modifyAccessControl, jcr:modifyProperties, jcr:namespaceManagement, jcr:nodeTypeDefinitionManagement, jcr:nodeTypeManagement, jcr:readAccessControl, jcr:removeChildNodes, jcr:removeNode, jcr:retentionManagement, jcr:versionManagement, jcr:workspaceManagement, jcr:write, rep:privilegeManagement`). References: | either actions or privileges need to be present; also a mix of both is possible +repGlob |A repGlob expression like "/jcr:*". Please note that repGlobs do not play well together with actions. Use privileges instead (e.g. "jcr:read" instead of read action). See [issue #48](https://github.com/Netcentric/accesscontroltool/issues/48) | no +initialContent | Allows to specify docview xml to create the path if it does not exist. The namespaces for jcr, sling and cq are added automatically if not provided to keep xml short. Initial content must only be specified exactly once per path (this is validated). If paths without permissions should be created, it is possible to provide only a path/initialContent tuple. Available form version 1.7.0. | no + +Every new data entry starts with a "-". + + +Overall format + +``` +[principal] + - path: a valid node path in CRX + permission: [allow/deny] + actions: actions string + privileges: privileges string + repGlob: regex (optional, path restriction as regular expression) + initialContent: (optional) +``` + +Only ACEs for groups which are defined in the same configuration file can be installed! This ensures a consistency between the groups and their ACE definitions per configuration file. + +Cq actions and jcr: privileges can be mixed. If jcr: privileges are already covered by cq actions within an ACE definition they get ignored. Also aggregated privileges like jcr:all or rep:write can be used. + +Example: + +``` +- ace_config: + + - fragment-allow: + + - path: /content/myproject + permission: allow + actions: read,modify,create,delete,acl_read,acl_edit,replicate + repGlob: */jcr:content* + + - path: '/content/myproject/*/articles' + permission: allow + actions: read,write + privileges: + + - path: /content/mydemoproject + permission: allow + actions: read,write + privileges: crx:replicate + + - fragment-deny: + + - path: / + permission: deny + actions: modify,create,delete,acl_read,acl_edit,replicate +``` + +In case the configuration file contains ACEs for groups which are not present in the current configuration no installation takes place and an appropriate error message gets displayed in the history log. + +All important steps performed by the service as well as all error/warning messages get written to error log and history. + +## Validation + +First the validation of the different configuration lines is performed and gets applied while reading the file. Further validation consists of checking paths for existence as well as for double entries, checks for conflicting ACEs (e.g. allow and deny for same actions on same node), checks whether principals are existing under /home. If an issue is detected the reading is aborted and an appropriate error message is appended to the installation history and log. + +If issues occur during the application of the configurations in CRX the installation has to be aborted and the previous state has to stay untouched. Therefore the session used for the installation only gets saved if no issues occurred thus persisting the changes. + diff --git a/docs/Jmx.md b/docs/Jmx.md new file mode 100644 index 00000000..b82aca31 --- /dev/null +++ b/docs/Jmx.md @@ -0,0 +1,62 @@ +# JMX interface + +The JMX interface offers the possibility to trigger the functions offered by the ACE service. These are: + +* starting a new installation of the newest configuration files in CRX. +* purging of ACLs (of a single node or recursively for all subnodes) +* deletion of single ACEs +* purging users/groups from the instance (including all related ACEs). +* creation of dumps (ordered by path or by group) +* showing of history logs created during the installation + +Also important status messages are shown here: + +* configuration files that are taken into account +* display of the paths of last 5 history logs saved in CTX and a success status of each of those installation +* readiness for installation (if at least one configuration file is stored in CRX) +* execution status of service + + + +## Operations + +### execute() + +This will install the configuration files listed on top respecting the [run mode semantics](Configuration.md). +Before installing a new configuration on an instance a validation of the data stored in the configuration file takes place. In case an issue gets detected the installation does not get performed. + +The path of the config files is configured in OSGi - AC Installation Service. + + + +### pathBasedDump() and groupBasedDump() + +* path based dumps: here all ACEs in the dump are grouped by path thus representing a complete ACL. This kind of dump gets triggered by the method: pathBasedDump(). +* group based dumps: here all ACEs in the dump are grouped by their respective principal (group or user). This kind of dump gets triggered by the method: groupBasedDump(). + +The created dump can be watched directly in JMX and also gets saved in CRX under /var/statistics/achistory/dump_[Timestamp]. The number of dumps to be saved in CRX can be configured in the OSGi configuration of the dump service in the field: "Number of dumps to save" (see screenshot). + + + +There are also some additional options available in the OSGi configuration of the dump service: + +* Include user in ACEs in dumps: if checked, all users which have ACEs directly set in the repository get added to the dump (ACEs in section "- ace_config:" and users in section "- user_config:") +* filtered dump: if checked, all ACEs belonging to the cq actions: modifiy, create or delete which have a repGlob set don't get added to the dump. Per default they are omitted since they're automatically set within CQ if one of the action gets set and are not necessary to expicitly being set in a new configuration file. +* include legacy ACEs in dump: if checked, legacy ACEs (ACEs of groups/users which can not be found under /home) also get added to the dump in an extra section called "legacy_aces" at the end of the dump. The order of ACEs listed there is the same as it is for the "valid" ACEs (path based or group based). If needed these ACEs can be manually deleted using the "purge_Authorizables" function by entering the respective principal name(s). +* AC query exclude paths: In order to exclude certain direct child nodes of the jcr:root node from the query (e.g. /home, since we do not want to have the rep:policy nodes of each user) to get all the rep:policy nodes in the system, these nodes can be configured. '/home', '/jcr:system' and '/tmp' are exclude by default. + +Internal dumps which get created and used everytime an new AC installation takes place get also created by this service and contain always all ACEs (users and unfiltered ACEs). + +### Purge ACLs/Authorizables + +Beside the options to install access control configurations and to create dumps the ACTool also offers different possibilities to purge ACLs/authorizables from the system. + +Method | Action +--- | --- +purgeACL | This method purges the access control list of a (single) node in repository. The node path is entered as parameter before invocation. +purgeACLs | This method purges the access control list of a node and also the access control lists of all child nodes. The node path is entered as parameter before invocation. +purgeAuthorizables | This method purges authorizables from home and also deletes all corresponding ACEs from the repository. Several authorizables are entered as comma separated list before invocation. +purgeAllAuthorizablesFromConfigurations | This method purges all authorizables defined in all configurations files and all their corresponding ACEs from the repository. + +For any of these purge actions a separate purge history (node) containing all logging statements gets persisted in CRX in order to be able to track every of those actions afterwards. Such a purge history node gets saved under the history node of the current ac installation in place. Any of these purge nodes has a timestamp as suffix in the node name. + diff --git a/docs/images/crx-storage.png b/docs/images/crx-storage.png index 8b291cbe..8d0b7f78 100644 Binary files a/docs/images/crx-storage.png and b/docs/images/crx-storage.png differ diff --git a/docs/images/installation-service.png b/docs/images/installation-service.png new file mode 100644 index 00000000..fa8b4110 Binary files /dev/null and b/docs/images/installation-service.png differ diff --git a/docs/images/upload-listener.png b/docs/images/upload-listener.png new file mode 100644 index 00000000..e06ba046 Binary files /dev/null and b/docs/images/upload-listener.png differ diff --git a/pom.xml b/pom.xml index f93d33e9..b9a5c0ed 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ biz.netcentric.cq.tools.accesscontroltool accesscontroltool - 1.8.3 + 1.8.4 pom Access Control Tool - Reactor Project @@ -76,6 +76,8 @@ accesscontroltool-bundle accesscontroltool-package + accesscontroltool-oakindex-package + accesscontroltool-exampleconfig-package @@ -435,13 +437,6 @@ - - oakindex - - accesscontroltool-oakindex-package - - -