22
33import com .fasterxml .jackson .databind .ObjectReader ;
44import com .fasterxml .jackson .databind .node .ObjectNode ;
5- import com .marklogic .appdeployer .command .*;
5+ import com .marklogic .appdeployer .command .AbstractResourceCommand ;
6+ import com .marklogic .appdeployer .command .CommandContext ;
7+ import com .marklogic .appdeployer .command .SortOrderConstants ;
8+ import com .marklogic .appdeployer .command .SupportsCmaCommand ;
69import com .marklogic .mgmt .SaveReceipt ;
7- import com .marklogic .mgmt .api .API ;
810import com .marklogic .mgmt .api .configuration .Configuration ;
911import com .marklogic .mgmt .api .configuration .Configurations ;
1012import com .marklogic .mgmt .api .security .Role ;
11- import com .marklogic .mgmt .mapper .DefaultResourceMapper ;
12- import com .marklogic .mgmt .mapper .ResourceMapper ;
13+ import com .marklogic .mgmt .api .security .RoleObjectNodesSorter ;
1314import com .marklogic .mgmt .resource .ResourceManager ;
1415import com .marklogic .mgmt .resource .security .RoleManager ;
1516import com .marklogic .mgmt .util .ObjectMapperFactory ;
17+ import com .marklogic .mgmt .util .ObjectNodesSorter ;
1618import com .marklogic .rest .util .ResourcesFragment ;
1719
1820import java .io .File ;
1921import java .io .IOException ;
2022import java .util .ArrayList ;
21- import java .util .HashSet ;
2223import java .util .List ;
23- import java .util .Set ;
2424
25+ /**
26+ * As of 3.15.0, this no longer deploys roles in two phases. This is due to the new sorting class, which uses a
27+ * topological sort to properly account for role dependencies.
28+ * <p>
29+ * However, to take advantage of this sorting, this class instructs the parent class to always construct a CMA request
30+ * for all of the roles that are found. This allows for all of the roles to be sorted easily, as they're in a list of
31+ * ObjectNode objects. Once the CMA request is ready to be submitted, this class then checks to see if CMA should
32+ * actually be used. If not, then each role is submitted individually in the correct order.
33+ */
2534public class DeployRolesCommand extends AbstractResourceCommand implements SupportsCmaCommand {
2635
27- // Used internally
28- private boolean removeRolesAndPermissionsDuringDeployment = false ;
29- private ResourceMapper resourceMapper ;
30- private Set <String > roleNamesThatDontNeedToBeRedeployed ;
31-
32- private boolean deployRolesInTwoPhases = true ;
36+ private ObjectNodesSorter objectNodesSorter = new RoleObjectNodesSorter ();
3337
3438 public DeployRolesCommand () {
3539 setExecuteSortOrder (SortOrderConstants .DEPLOY_ROLES );
@@ -40,9 +44,21 @@ public DeployRolesCommand() {
4044 setResourceClassType (Role .class );
4145 }
4246
47+ /**
48+ * This tells the parent class to always build a Configuration, even if CMA isn't available. When it's time to
49+ * deploy the configuration, we'll check to see if CMA truly is available.
50+ *
51+ * @param context
52+ * @return
53+ */
54+ @ Override
55+ protected boolean useCmaForDeployingResources (CommandContext context ) {
56+ return true ;
57+ }
58+
4359 @ Override
4460 public boolean cmaShouldBeUsed (CommandContext context ) {
45- return context . getAppConfig (). getCmaConfig (). isDeployRoles () ;
61+ return true ;
4662 }
4763
4864 @ Override
@@ -51,162 +67,107 @@ public void addResourceToConfiguration(ObjectNode resource, Configuration config
5167 }
5268
5369 /**
54- * The set of roles is processed twice. The first time, the roles are saved without any default permissions or references to other roles.
55- * This is to avoid issues where the roles refer to each other or to themselves (via default permissions). The second time, the roles are
56- * saved with permissions and references to other roles, which is guaranteed to work now that the roles have all been created.
70+ * Before a role configuration can be submitted, the roles within the configuration must be sorted based on their
71+ * dependencies.
5772 * <p>
58- * For 3.11.0, as part of the new preview feature, the boolean deployRolesInTwoPhases has been added so that the
59- * process of deploying roles in two phases can be disabled during a preview.
73+ * Then, if CMA is available and the user wants it to be used, the configuration is either submitted or added to a
74+ * combined CMA request. Otherwise, each role will be created individually. In both cases, a check is made on
75+ * each role to see if it does not exist yet and refers to itself. If so, such roles will be created first without
76+ * any dependencies.
6077 *
6178 * @param context
79+ * @param config
6280 */
6381 @ Override
64- public void execute (CommandContext context ) {
65- roleNamesThatDontNeedToBeRedeployed = new HashSet <>();
66- if (deployRolesInTwoPhases && !cmaShouldBeUsed (context )) {
67- removeRolesAndPermissionsDuringDeployment = true ;
68- if (logger .isInfoEnabled ()) {
69- logger .info ("Deploying roles minus their default permissions and references to roles" );
70- }
71- super .execute (context );
72- if (logger .isInfoEnabled ()) {
73- logger .info ("Redeploying roles that have default permissions and/or references to roles" );
74- }
75- removeRolesAndPermissionsDuringDeployment = false ;
82+ protected void deployConfiguration (CommandContext context , Configuration config ) {
83+ List <ObjectNode > roleNodes = config .getRoles ();
84+ if (roleNodes == null || roleNodes .isEmpty ()) {
85+ return ;
7686 }
7787
78- super .execute (context );
79- }
80-
81- @ Override
82- protected void deployConfiguration (CommandContext context , Configuration config ) {
83- submitConfigurationWithRolesThatReferenceThemselves (context , config );
88+ if (objectNodesSorter != null ) {
89+ logger .info ("Sorting roles before they are saved" );
90+ roleNodes = objectNodesSorter .sortObjectNodes (roleNodes );
91+ config .setRoles (roleNodes );
92+ }
8493
85- if (config .getRoles () != null ) {
86- logger .info ("Sorting roles before they are submitted in a CMA request" );
87- config .setRoles (Role .sortObjectNodes (config .getRoles ()));
94+ if (context .getAppConfig ().getCmaConfig ().isDeployRoles () && cmaEndpointExists (context )) {
95+ submitRolesConfigurationViaCma (context , config );
96+ } else {
97+ submitRolesIndividually (context , roleNodes );
8898 }
99+ }
89100
101+ protected void submitRolesConfigurationViaCma (CommandContext context , Configuration config ) {
102+ submitConfigurationWithRolesThatReferenceThemselves (context , config .getRoles ());
90103 if (context .getAppConfig ().getCmaConfig ().isCombineRequests ()) {
91104 logger .info ("Adding roles to combined CMA request" );
92105 context .addCmaConfigurationToCombinedRequest (config );
93106 } else {
94- final String key = getClass ().getSimpleName () + "-roles" ;
95- if (removeRolesAndPermissionsDuringDeployment ) {
96- context .getContextMap ().put (key , new Configurations (config ));
97- } else {
98- Configurations configs = (Configurations ) context .getContextMap ().get (key );
99- if (configs == null ) {
100- // This shouldn't ever happen, but just in case
101- configs = new Configurations ();
102- }
103- configs .getConfigs ().add (config );
104- configs .submit (context .getManageClient ());
105- }
107+ super .deployConfiguration (context , config );
106108 }
107109 }
108110
111+ protected void submitRolesIndividually (CommandContext context , List <ObjectNode > roleNodes ) {
112+ RoleManager roleManager = new RoleManager (context .getManageClient ());
113+
114+ findRolesThatReferenceThemselves (context , roleNodes ).forEach (role -> {
115+ roleManager .save (format ("{\" role-name\" :\" %s\" }" , role .getRoleName ()));
116+ });
117+
118+ roleNodes .forEach (roleNode -> {
119+ SaveReceipt receipt = saveResource (roleManager , context , roleNode .toString ());
120+ afterResourceSaved (roleManager , context , null , receipt );
121+ });
122+ }
123+
109124 /**
110125 * If a role refers to itself via permissions, that role won't be created by CMA. Instead, a separate CMA request
111126 * is constructed, with each such role only having a role-name, and then immediately submitted so that the roles
112127 * are guaranteed to exist. Note that only roles that don't exist yet will be included in this request (if they
113128 * already exist, then no problem will occur).
114129 *
115130 * @param context
116- * @param config
131+ * @param roles
117132 */
118- protected void submitConfigurationWithRolesThatReferenceThemselves (CommandContext context , Configuration config ) {
119- List <ObjectNode > roles = config .getRoles ();
120- if (roles != null && !roles .isEmpty ()) {
121- ObjectReader reader = ObjectMapperFactory .getObjectMapper ().readerFor (Role .class );
122- List <ObjectNode > rolesThatReferenceThemselves = new ArrayList <>();
123- ResourcesFragment rolesXml = new RoleManager (context .getManageClient ()).getAsXml ();
124-
125- roles .forEach (role -> {
126- try {
127- Role r = reader .readValue (role );
128- if (r .hasPermissionWithOwnRoleName () && !rolesXml .resourceExists (r .getRoleName ())) {
129- ObjectNode node = ObjectMapperFactory .getObjectMapper ().createObjectNode ();
130- node .put ("role-name" , r .getRoleName ());
131- rolesThatReferenceThemselves .add (node );
132- }
133- } catch (IOException e ) {
134- throw new RuntimeException ("Unable to read ObjectNode into Role; node: " + role , e );
135- }
133+ protected void submitConfigurationWithRolesThatReferenceThemselves (CommandContext context , List <ObjectNode > roles ) {
134+ List <Role > rolesThatReferenceThemselves = findRolesThatReferenceThemselves (context , roles );
135+
136+ if (!rolesThatReferenceThemselves .isEmpty ()) {
137+ Configuration roleNamesOnlyConfig = new Configuration ();
138+ rolesThatReferenceThemselves .forEach (role -> {
139+ ObjectNode node = ObjectMapperFactory .getObjectMapper ().createObjectNode ();
140+ node .put ("role-name" , role .getRoleName ());
141+ roleNamesOnlyConfig .addRole (node );
136142 });
137-
138- if (!rolesThatReferenceThemselves .isEmpty ()) {
139- Configuration roleNamesOnlyConfig = new Configuration ();
140- rolesThatReferenceThemselves .forEach (role -> roleNamesOnlyConfig .addRole (role ));
141- logger .info ("Submitting CMA configuration containing roles that reference themselves and do not yet exist" );
142- new Configurations (roleNamesOnlyConfig ).submit (context .getManageClient ());
143- }
143+ logger .info ("Submitting CMA configuration containing roles that reference themselves and do not yet exist" );
144+ new Configurations (roleNamesOnlyConfig ).submit (context .getManageClient ());
144145 }
145146 }
146147
147148 /**
148- * If the resource was saved during the first pass - i.e. when roles and permissions have been removed from the role
149- * - then it must be processed during the second pass so that those roles/permissions can be added back. Thus, the
150- * incremental check on the file must be ignored. Note that this only matters during an incremental deploy, which
151- * means that the ResourceReference should have a single File in it.
152- */
153- @ Override
154- protected void afterResourceSaved (ResourceManager mgr , CommandContext context , ResourceReference resourceReference , SaveReceipt receipt ) {
155- if (removeRolesAndPermissionsDuringDeployment && resourceReference != null ) {
156- ignoreIncrementalCheckForFile (resourceReference .getLastFile ());
157- }
158- super .afterResourceSaved (mgr , context , resourceReference , receipt );
159- }
160-
161- /**
162- * If this is the first time roles are being deployed by this command - indicated by the removeRolesAndPermissionsDuringDeployment
163- * class variable - then each payload is modified so that default permissions and role references are not included,
164- * thus ensuring that the role can be created successfully.
165- * <p>
166- * If this is the second time that roles are being deployed by this command, then the entire payload is sent. However,
167- * if the role doesn't have any default permissions or role references, it will not be deployed a second time, as
168- * there was nothing missing from the first deployment of the role.
149+ * Returns a list of roles, one for each role in the given ObjectNode list that does not exist yet and refers to
150+ * itself.
169151 *
170- * @param mgr
171152 * @param context
172- * @param f
173- * @param payload
153+ * @param roles
174154 * @return
175155 */
176- @ Override
177- protected String adjustPayloadBeforeSavingResource (CommandContext context , File f , String payload ) {
178- payload = super .adjustPayloadBeforeSavingResource (context , f , payload );
179-
180- if (resourceMapper == null ) {
181- API api = new API (context .getManageClient (), context .getAdminManager ());
182- resourceMapper = new DefaultResourceMapper (api );
183- }
184-
185- Role role = resourceMapper .readResource (payload , Role .class );
186-
187- // Is this the first time the roles are being deployed?
188- if (removeRolesAndPermissionsDuringDeployment ) {
189- if (role .hasPermissionsOrRoles ()) {
190- role .clearPermissionsAndRoles ();
191- return role .getJson ();
192- } else {
193- roleNamesThatDontNeedToBeRedeployed .add (role .getRoleName ());
194- return payload ;
195- }
196- }
197- // Else it's the second time roles are being deployed, but no need to deploy a role if it doesn't have any default permissions or role references
198- else if (roleNamesThatDontNeedToBeRedeployed .contains (role .getRoleName ())) {
199- if (logger .isInfoEnabled ()) {
200- logger .info ("Not redeploying role " + role .getRoleName () + ", as it does not have any default permissions or references to other roles" );
156+ protected List <Role > findRolesThatReferenceThemselves (CommandContext context , List <ObjectNode > roles ) {
157+ ObjectReader reader = ObjectMapperFactory .getObjectMapper ().readerFor (Role .class );
158+ List <Role > rolesThatReferenceThemselves = new ArrayList <>();
159+ ResourcesFragment rolesXml = new RoleManager (context .getManageClient ()).getAsXml ();
160+ roles .forEach (role -> {
161+ try {
162+ Role r = reader .readValue (role );
163+ if (r .hasPermissionWithOwnRoleName () && !rolesXml .resourceExists (r .getRoleName ())) {
164+ rolesThatReferenceThemselves .add (r );
165+ }
166+ } catch (IOException e ) {
167+ throw new RuntimeException ("Unable to read ObjectNode into Role; node: " + role , e );
201168 }
202- return null ;
203- }
204- // Else log a message to indicate that the role is being redeployed
205- else if (logger .isInfoEnabled ()) {
206- logger .info ("Redeploying role " + role .getRoleName () + " with default permissions and references to other roles included" );
207- }
208-
209- return payload ;
169+ });
170+ return rolesThatReferenceThemselves ;
210171 }
211172
212173 protected File [] getResourceDirs (CommandContext context ) {
@@ -218,8 +179,13 @@ protected ResourceManager getResourceManager(CommandContext context) {
218179 return new RoleManager (context .getManageClient ());
219180 }
220181
182+ @ Deprecated
221183 public void setDeployRolesInTwoPhases (boolean deployRolesInTwoPhases ) {
222- this .deployRolesInTwoPhases = deployRolesInTwoPhases ;
184+ // No longer needed as of 3.15.0
185+ }
186+
187+ public void setObjectNodesSorter (ObjectNodesSorter objectNodesSorter ) {
188+ this .objectNodesSorter = objectNodesSorter ;
223189 }
224190}
225191
0 commit comments