Merge "PolicyAudit creation when deploy/undeploy triggered."
[policy/pap.git] / main / src / main / java / org / onap / policy / pap / main / rest / PdpGroupDeployProvider.java
1 /*
2  * ============LICENSE_START=======================================================
3  * ONAP PAP
4  * ================================================================================
5  * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2020-2021 Nordix Foundation.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21
22 package org.onap.policy.pap.main.rest;
23
24 import com.google.gson.annotations.SerializedName;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.LinkedHashSet;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.stream.Collectors;
33 import javax.ws.rs.core.Response.Status;
34 import lombok.Getter;
35 import org.onap.policy.common.parameters.BeanValidationResult;
36 import org.onap.policy.common.parameters.BeanValidator;
37 import org.onap.policy.common.parameters.ValidationResult;
38 import org.onap.policy.common.parameters.ValidationStatus;
39 import org.onap.policy.common.parameters.annotations.NotNull;
40 import org.onap.policy.common.parameters.annotations.Pattern;
41 import org.onap.policy.common.parameters.annotations.Valid;
42 import org.onap.policy.common.utils.coder.CoderException;
43 import org.onap.policy.common.utils.coder.StandardCoder;
44 import org.onap.policy.common.utils.services.Registry;
45 import org.onap.policy.models.base.PfKey;
46 import org.onap.policy.models.base.PfModelException;
47 import org.onap.policy.models.base.PfModelRuntimeException;
48 import org.onap.policy.models.pap.concepts.PdpDeployPolicies;
49 import org.onap.policy.models.pdp.concepts.DeploymentGroup;
50 import org.onap.policy.models.pdp.concepts.DeploymentGroups;
51 import org.onap.policy.models.pdp.concepts.DeploymentSubGroup;
52 import org.onap.policy.models.pdp.concepts.Pdp;
53 import org.onap.policy.models.pdp.concepts.PdpGroup;
54 import org.onap.policy.models.pdp.concepts.PdpSubGroup;
55 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
56 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifierOptVersion;
57 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 /**
62  * Provider for PAP component to deploy PDP groups. The following items must be in the
63  * {@link Registry}:
64  * <ul>
65  * <li>PDP Modification Lock</li>
66  * <li>PDP Modify Request Map</li>
67  * <li>PAP DAO Factory</li>
68  * </ul>
69  */
70 public class PdpGroupDeployProvider extends ProviderBase {
71     private static final Logger logger = LoggerFactory.getLogger(PdpGroupDeployProvider.class);
72     private static final StandardCoder coder = new StandardCoder();
73
74     private static final String POLICY_RESULT_NAME = "policy";
75
76
77     /**
78      * Constructs the object.
79      */
80     public PdpGroupDeployProvider() {
81         super();
82     }
83
84     /**
85      * Updates policies in specific PDP groups.
86      *
87      * @param groups PDP group deployments to be updated
88      * @param user user triggering deployment
89      * @throws PfModelException if an error occurred
90      */
91     public void updateGroupPolicies(DeploymentGroups groups, String user) throws PfModelException {
92         ValidationResult result = groups.validatePapRest();
93         if (!result.isValid()) {
94             String msg = result.getResult().trim();
95             throw new PfModelException(Status.BAD_REQUEST, msg);
96         }
97
98         process(user, groups, this::updateGroups);
99     }
100
101     /**
102      * Updates policies in specific PDP groups. This is the method that does the actual work.
103      *
104      * @param data session data
105      * @param groups PDP group deployments
106      * @throws PfModelException if an error occurred
107      */
108     private void updateGroups(SessionData data, DeploymentGroups groups) throws PfModelException {
109         var result = new BeanValidationResult("groups", groups);
110
111         for (DeploymentGroup group : groups.getGroups()) {
112             PdpGroup dbgroup = data.getGroup(group.getName());
113
114             if (dbgroup == null) {
115                 result.addResult(group.getName(), group, ValidationStatus.INVALID, "unknown group");
116
117             } else {
118                 result.addResult(updateGroup(data, dbgroup, group));
119             }
120         }
121
122         if (!result.isValid()) {
123             throw new PfModelException(Status.BAD_REQUEST, result.getResult().trim());
124         }
125     }
126
127     /**
128      * Updates an existing group.
129      *
130      * @param data session data
131      * @param dbgroup the group, as it appears within the DB
132      * @param group the group to be added
133      * @return the validation result
134      * @throws PfModelException if an error occurred
135      */
136     private ValidationResult updateGroup(SessionData data, PdpGroup dbgroup, DeploymentGroup group)
137                     throws PfModelException {
138
139         var result = new BeanValidationResult(group.getName(), group);
140
141         boolean updated = updateSubGroups(data, dbgroup, group, result);
142
143         if (result.isValid() && updated) {
144             data.update(dbgroup);
145         }
146
147         return result;
148     }
149
150     /**
151      * Adds or updates subgroups within the group.
152      *
153      * @param data session data
154      * @param dbgroup the group, as it appears within the DB
155      * @param group the group to be added
156      * @param result the validation result
157      * @return {@code true} if the DB group was modified, {@code false} otherwise
158      * @throws PfModelException if an error occurred
159      */
160     private boolean updateSubGroups(SessionData data, PdpGroup dbgroup, DeploymentGroup group,
161                     BeanValidationResult result) throws PfModelException {
162
163         // create a map of existing subgroups
164         Map<String, PdpSubGroup> type2sub = new HashMap<>();
165         dbgroup.getPdpSubgroups().forEach(subgrp -> type2sub.put(subgrp.getPdpType(), subgrp));
166
167         var updated = false;
168
169         for (DeploymentSubGroup subgrp : group.getDeploymentSubgroups()) {
170             PdpSubGroup dbsub = type2sub.get(subgrp.getPdpType());
171             var subResult = new BeanValidationResult(subgrp.getPdpType(), subgrp);
172
173             if (dbsub == null) {
174                 subResult.addResult(subgrp.getPdpType(), subgrp, ValidationStatus.INVALID, "unknown subgroup");
175
176             } else {
177                 updated = updateSubGroup(data, dbgroup, dbsub, subgrp, subResult) || updated;
178             }
179
180             result.addResult(subResult);
181         }
182
183         return updated;
184     }
185
186     /**
187      * Updates an existing subgroup.
188      *
189      * @param data session data
190      * @param dbgroup the group, from the DB, containing the subgroup
191      * @param dbsub the subgroup, from the DB
192      * @param subgrp the subgroup to be updated, updated to fully qualified versions upon
193      *        return
194      * @param container container for additional validation results
195      * @return {@code true} if the subgroup content was changed, {@code false} if there
196      *         were no changes
197      * @throws PfModelException if an error occurred
198      */
199     private boolean updateSubGroup(SessionData data, PdpGroup dbgroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
200                     BeanValidationResult container) throws PfModelException {
201
202         // perform additional validations first
203         if (!validateSubGroup(data, dbsub, subgrp, container)) {
204             return false;
205         }
206
207         var updated = false;
208
209         switch (subgrp.getAction()) {
210             case POST:
211                 updated = addPolicies(data, dbgroup.getName(), dbsub, subgrp);
212                 break;
213             case DELETE:
214                 updated = deletePolicies(data, dbgroup.getName(), dbsub, subgrp);
215                 break;
216             case PATCH:
217             default:
218                 updated = updatePolicies(data, dbgroup.getName(), dbsub, subgrp);
219                 break;
220         }
221
222         if (updated) {
223             // publish any changes to the PDPs
224             makeUpdates(data, dbgroup, dbsub);
225             return true;
226         }
227
228         return false;
229     }
230
231     private boolean addPolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
232                     throws PfModelException {
233
234         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
235         policies.addAll(subgrp.getPolicies());
236
237         var subgrp2 = new DeploymentSubGroup(subgrp);
238         subgrp2.getPolicies().clear();
239         subgrp2.getPolicies().addAll(policies);
240
241         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
242     }
243
244     private boolean deletePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
245                     throws PfModelException {
246
247         Set<ToscaConceptIdentifier> policies = new LinkedHashSet<>(dbsub.getPolicies());
248         policies.removeAll(subgrp.getPolicies());
249
250         var subgrp2 = new DeploymentSubGroup(subgrp);
251         subgrp2.getPolicies().clear();
252         subgrp2.getPolicies().addAll(policies);
253
254         return updatePolicies(data, pdpGroup, dbsub, subgrp2);
255     }
256
257     private boolean updatePolicies(SessionData data, String pdpGroup, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
258                     throws PfModelException {
259
260         Set<ToscaConceptIdentifier> undeployed = new HashSet<>(dbsub.getPolicies());
261         undeployed.removeAll(subgrp.getPolicies());
262
263         Set<ToscaConceptIdentifier> deployed = new HashSet<>(subgrp.getPolicies());
264         deployed.removeAll(dbsub.getPolicies());
265
266         if (deployed.isEmpty() && undeployed.isEmpty()) {
267             // lists are identical
268             return false;
269         }
270
271         Set<String> pdps = dbsub.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
272
273         for (ToscaConceptIdentifier policyId : deployed) {
274             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(policyId));
275             data.trackDeploy(policyToBeDeployed, pdps, pdpGroup, dbsub.getPdpType());
276         }
277
278         for (ToscaConceptIdentifier policyId : undeployed) {
279             data.trackUndeploy(policyId, pdps, pdpGroup, dbsub.getPdpType());
280         }
281
282         dbsub.setPolicies(new ArrayList<>(subgrp.getPolicies()));
283         return true;
284     }
285
286     /**
287      * Performs additional validations of a subgroup.
288      *
289      * @param data session data
290      * @param dbsub the subgroup, from the DB
291      * @param subgrp the subgroup to be validated, updated to fully qualified versions
292      *        upon return
293      * @param container container for additional validation results
294      * @return {@code true} if the subgroup is valid, {@code false} otherwise
295      * @throws PfModelException if an error occurred
296      */
297     private boolean validateSubGroup(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp,
298                     BeanValidationResult container) throws PfModelException {
299
300         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
301
302         result.addResult(validatePolicies(data, dbsub, subgrp));
303         container.addResult(result);
304
305         return result.isValid();
306     }
307
308     /**
309      * Performs additional validations of the policies within a subgroup.
310      *
311      * @param data session data
312      * @param dbsub subgroup from the DB, or {@code null} if this is a new subgroup
313      * @param subgrp the subgroup whose policies are to be validated, updated to fully
314      *        qualified versions upon return
315      * @return the validation result
316      * @throws PfModelException if an error occurred
317      */
318     private ValidationResult validatePolicies(SessionData data, PdpSubGroup dbsub, DeploymentSubGroup subgrp)
319                     throws PfModelException {
320
321         // build a map of the DB data, from policy name to (fully qualified) policy
322         // version
323         Map<String, String> dbname2vers = new HashMap<>();
324         dbsub.getPolicies().forEach(ident -> dbname2vers.put(ident.getName(), ident.getVersion()));
325
326         var result = new BeanValidationResult(subgrp.getPdpType(), subgrp);
327
328         for (ToscaConceptIdentifier ident : subgrp.getPolicies()) {
329             // note: "ident" may not have a fully qualified version
330
331             String expectedVersion = dbname2vers.get(ident.getName());
332             if (expectedVersion != null) {
333                 // policy exists in the DB list - compare the versions
334                 validateVersion(expectedVersion, ident, result);
335                 ident.setVersion(expectedVersion);
336                 continue;
337             }
338
339             // policy doesn't appear in the DB's policy list - look it up
340
341             ToscaPolicy policy = data.getPolicy(new ToscaConceptIdentifierOptVersion(ident));
342             if (policy == null) {
343                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID, "unknown policy");
344
345             } else if (!isPolicySupported(dbsub.getSupportedPolicyTypes(), policy.getTypeIdentifier())) {
346                 result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
347                                 "not a supported policy for the subgroup");
348
349             } else {
350                 // replace version with the fully qualified version from the policy
351                 ident.setVersion(policy.getVersion());
352             }
353         }
354
355         return result;
356     }
357
358     /**
359      * Determines if the new version matches the version in the DB.
360      *
361      * @param dbvers fully qualified version from the DB
362      * @param ident identifier whose version is to be validated; the version need not be
363      *        fully qualified
364      * @param result the validation result
365      */
366     private void validateVersion(String dbvers, ToscaConceptIdentifier ident, BeanValidationResult result) {
367         String idvers = ident.getVersion();
368         if (dbvers.equals(idvers)) {
369             return;
370         }
371
372         // did not match - see if it's a prefix
373
374         if (SessionData.isVersionPrefix(idvers) && dbvers.startsWith(idvers + ".")) {
375             // ident has a prefix of this version
376             return;
377         }
378
379         result.addResult(POLICY_RESULT_NAME, ident, ValidationStatus.INVALID,
380                         "different version already deployed: " + dbvers);
381     }
382
383     /**
384      * Deploys or updates PDP policies using the simple API.
385      *
386      * @param policies PDP policies
387      * @param user user triggering deployment
388      * @throws PfModelException if an error occurred
389      */
390     public void deployPolicies(PdpDeployPolicies policies, String user) throws PfModelException {
391         try {
392             MyPdpDeployPolicies checked = coder.convert(policies, MyPdpDeployPolicies.class);
393             ValidationResult result = new BeanValidator().validateTop(PdpDeployPolicies.class.getSimpleName(), checked);
394             if (!result.isValid()) {
395                 String msg = result.getResult().trim();
396                 throw new PfModelException(Status.BAD_REQUEST, msg);
397             }
398         } catch (CoderException e) {
399             throw new PfModelException(Status.INTERNAL_SERVER_ERROR, "cannot decode request", e);
400         }
401
402         process(user, policies, this::deploySimplePolicies);
403     }
404
405     /**
406      * Deploys or updates PDP policies using the simple API. This is the method that does
407      * the actual work.
408      *
409      * @param data session data
410      * @param policies external PDP policies
411      * @throws PfModelException if an error occurred
412      */
413     private void deploySimplePolicies(SessionData data, PdpDeployPolicies policies) throws PfModelException {
414
415         for (ToscaConceptIdentifierOptVersion desiredPolicy : policies.getPolicies()) {
416
417             try {
418                 processPolicy(data, desiredPolicy);
419
420             } catch (PfModelException | RuntimeException e) {
421                 // no need to log the error here, as it will be logged by the invoker
422                 logger.warn("failed to deploy policy: {}", desiredPolicy);
423                 throw e;
424             }
425         }
426     }
427
428     /**
429      * Adds a policy to a subgroup, if it isn't there already.
430      */
431     @Override
432     protected Updater makeUpdater(SessionData data, ToscaPolicy policy,
433                     ToscaConceptIdentifierOptVersion requestedIdent) {
434
435         ToscaConceptIdentifier desiredIdent = policy.getIdentifier();
436         ToscaConceptIdentifier desiredType = policy.getTypeIdentifier();
437
438         return (group, subgroup) -> {
439
440             if (!isPolicySupported(subgroup.getSupportedPolicyTypes(), desiredType)) {
441                 // doesn't support the desired policy type
442                 return false;
443             }
444
445             if (containsPolicy(group, subgroup, desiredIdent)) {
446                 return false;
447             }
448
449             if (subgroup.getPdpInstances().isEmpty()) {
450                 throw new PfModelRuntimeException(Status.BAD_REQUEST, "group " + group.getName() + " subgroup "
451                                 + subgroup.getPdpType() + " has no active PDPs");
452             }
453
454
455             // add the policy to the subgroup
456             subgroup.getPolicies().add(desiredIdent);
457
458             logger.info("add policy {} to subgroup {} {} count={}", desiredIdent, group.getName(),
459                             subgroup.getPdpType(), subgroup.getPolicies().size());
460
461             Set<String> pdps = subgroup.getPdpInstances().stream().map(Pdp::getInstanceId).collect(Collectors.toSet());
462             ToscaPolicy policyToBeDeployed = data.getPolicy(new ToscaConceptIdentifierOptVersion(desiredIdent));
463             data.trackDeploy(policyToBeDeployed, pdps, group.getName(), subgroup.getPdpType());
464
465             return true;
466         };
467     }
468
469     /**
470      * Determines if a policy type is supported.
471      *
472      * @param supportedTypes supported policy types, any of which may end with ".*"
473      * @param desiredType policy type of interest
474      * @return {@code true} if the policy type is supported, {@code false} otherwise
475      */
476     private boolean isPolicySupported(List<ToscaConceptIdentifier> supportedTypes,
477                     ToscaConceptIdentifier desiredType) {
478
479         if (supportedTypes.contains(desiredType)) {
480             return true;
481         }
482
483         String desiredTypeName = desiredType.getName();
484         for (ToscaConceptIdentifier type : supportedTypes) {
485             String supType = type.getName();
486             if (supType.endsWith(".*") && desiredTypeName.startsWith(supType.substring(0, supType.length() - 1))) {
487                 // matches everything up to, AND INCLUDING, the "."
488                 return true;
489             }
490         }
491
492         return false;
493     }
494
495     /**
496      * Determines if a subgroup already contains the desired policy.
497      *
498      * @param group group that contains the subgroup
499      * @param subgroup subgroup of interest
500      * @param desiredIdent identifier of the desired policy
501      * @return {@code true} if the subgroup contains the desired policy, {@code false}
502      *         otherwise
503      * @throws PfModelRuntimeException if the subgroup contains a different version of the
504      *         desired policy
505      */
506     private boolean containsPolicy(PdpGroup group, PdpSubGroup subgroup, ToscaConceptIdentifier desiredIdent) {
507         String desnm = desiredIdent.getName();
508         String desvers = desiredIdent.getVersion();
509
510         for (ToscaConceptIdentifier actualIdent : subgroup.getPolicies()) {
511             if (!actualIdent.getName().equals(desnm)) {
512                 continue;
513             }
514
515             // found the policy - ensure the version matches
516             if (!actualIdent.getVersion().equals(desvers)) {
517                 throw new PfModelRuntimeException(Status.BAD_REQUEST,
518                                 "group " + group.getName() + " subgroup " + subgroup.getPdpType() + " policy " + desnm
519                                                 + " " + desvers + " different version already deployed: "
520                                                 + actualIdent.getVersion());
521             }
522
523             // already has the desired policy & version
524             logger.info("subgroup {} {} already contains policy {}", group.getName(), subgroup.getPdpType(),
525                         desiredIdent);
526             return true;
527         }
528
529         return false;
530     }
531
532     /*
533      * These are only used to validate the incoming request.
534      */
535
536     @Getter
537     public static class MyPdpDeployPolicies {
538         @NotNull
539         private List<@NotNull @Valid PolicyIdent> policies;
540     }
541
542     @Getter
543     public static class PolicyIdent {
544         @SerializedName("policy-id")
545         @NotNull
546         @Pattern(regexp = PfKey.NAME_REGEXP)
547         private String name;
548
549         @SerializedName("policy-version")
550         @Pattern(regexp = "\\d+([.]\\d+[.]\\d+)?")
551         private String version;
552     }
553 }