001/*
002 * The Kuali Financial System, a comprehensive financial management system for higher education.
003 * 
004 * Copyright 2005-2014 The Kuali Foundation
005 * 
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Affero General Public License as
008 * published by the Free Software Foundation, either version 3 of the
009 * License, or (at your option) any later version.
010 * 
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Affero General Public License for more details.
015 * 
016 * You should have received a copy of the GNU Affero General Public License
017 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
018 */
019package org.kuali.rice.kim.impl.jaxb;
020
021import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;
022
023import java.util.HashMap;
024import java.util.List;
025import java.util.Set;
026
027import javax.xml.bind.UnmarshalException;
028
029import org.apache.commons.lang.StringUtils;
030import org.joda.time.DateTime;
031import org.kuali.rice.core.api.criteria.QueryByCriteria;
032import org.kuali.rice.core.api.membership.MemberType;
033import org.kuali.rice.core.util.jaxb.NameAndNamespacePair;
034import org.kuali.rice.kim.api.group.GroupContract;
035import org.kuali.rice.kim.api.identity.principal.PrincipalContract;
036import org.kuali.rice.kim.api.permission.PermissionContract;
037import org.kuali.rice.kim.api.role.Role;
038import org.kuali.rice.kim.api.role.RoleContract;
039import org.kuali.rice.kim.api.role.RoleMember;
040import org.kuali.rice.kim.api.role.RoleMemberContract;
041import org.kuali.rice.kim.api.role.RoleService;
042import org.kuali.rice.kim.api.services.KimApiServiceLocator;
043
044/**
045 * Helper class containing static methods for aiding in parsing role XML.
046 *
047 * <p>All non-private methods are package-private so that only the KIM-parsing-related code can make use of them. (TODO: Is that necessary?)
048 *
049 * <p>TODO: Should this be converted into a service instead?
050 *
051 * @author Kuali Rice Team (rice.collab@kuali.org)
052 */
053public final class RoleXmlUtil {
054    // Do not allow outside code to instantiate this class.
055    private RoleXmlUtil() {}
056
057    /**
058     * Performs the necessary validation on the new role, then saves it.
059     *
060     * @param newRole The role to persist.
061     * @return The ID of the persisted role.
062     * @throws IllegalArgumentException if newRole is null.
063     * @throws UnmarshalException if newRole contains invalid data.
064     */
065    static String validateAndPersistNewRole(RoleXmlDTO newRole) throws UnmarshalException {
066        if (newRole == null) {
067            throw new IllegalArgumentException("Cannot persist a null role");
068        }
069
070        // Validate the role and (if applicable) retrieve the ID from an existing matching role.
071        validateAndPrepareRole(newRole);
072
073        Role.Builder builder = Role.Builder.create();
074        builder.setActive(newRole.getActive());
075        builder.setDescription(newRole.getRoleDescription());
076        builder.setId(newRole.getRoleId());
077        builder.setKimTypeId(newRole.getKimTypeId());
078        builder.setName(newRole.getRoleName());
079        builder.setNamespaceCode(newRole.getNamespaceCode());
080
081        //save the role
082        Role role = KimApiServiceLocator.getRoleService().createRole(builder.build());
083
084        // Set a flag on the role to indicate that it has now been persisted so that the unmarshalling process will not save this role more than once.
085        newRole.setAlreadyPersisted(true);
086
087        return role.getId();
088    }
089
090    /**
091     * Performs the necessary validation on the new role member, then saves it.
092     *
093     * @param newRoleMember The role member to save.
094     * @return The ID of the persisted role member.
095     * @throws IllegalArgumentException if newRoleMember is null.
096     * @throws UnmarshalException if newRoleMember contains invalid data.
097     */
098    static String validateAndPersistNewRoleMember(RoleMemberXmlDTO newRoleMember) throws UnmarshalException {
099
100        if (newRoleMember == null) {
101            throw new IllegalArgumentException("Cannot persist a null role member");
102        }
103
104        // Validate role ID and role name/namespace.
105        validateRoleIdAndRoleNameForMember(newRoleMember);
106
107        // Validate member type, member ID, and member name/namespace.
108        validateMemberIdentity(newRoleMember);
109
110        // Validate the from/to dates, if defined.
111        if (newRoleMember.getActiveFromDate() != null && newRoleMember.getActiveToDate() != null &&
112                newRoleMember.getActiveFromDate().compareTo(newRoleMember.getActiveToDate()) > 0) {
113            throw new UnmarshalException("Cannot create a role member whose activeFromDate occurs after its activeToDate");
114        }
115
116        // Define defaults as needed.
117        if (newRoleMember.getQualifications() == null) {
118            newRoleMember.setQualifications(new HashMap<String, String>());
119        }
120
121        RoleMember.Builder builder = RoleMember.Builder.create(newRoleMember.getRoleId(), newRoleMember.getRoleIdAsMember(),
122                newRoleMember.getMemberId(), newRoleMember.getMemberType(),
123                newRoleMember.getActiveFromDate() == null ? null : new DateTime(newRoleMember.getActiveFromDate().getMillis()),
124                newRoleMember.getActiveToDate() == null ? null : new DateTime(newRoleMember.getActiveToDate().getMillis()),
125                newRoleMember.getQualifications(), null, null);
126
127        // Save the role member.
128        RoleMemberContract newMember = KimApiServiceLocator.getRoleService().createRoleMember(builder.build());
129
130        return newMember.getId();
131    }
132
133    /**
134     * Performs the necessary validation on the role permission, then saves it.
135     *
136     * @param newRolePermission The role permission to save.
137     * @throws IllegalArgumentException if newRolePermission is null
138     * @throws UnmarshalException if newRolePermission contains invalid data.
139     */
140    static void validateAndPersistNewRolePermission(RolePermissionXmlDTO newRolePermission) throws UnmarshalException {
141        if (newRolePermission == null) {
142            throw new IllegalArgumentException("newRolePermission cannot be null");
143        }
144
145        // Validate the role permission, and prepare its role ID if necessary.
146        validateAndPrepareRolePermission(newRolePermission);
147
148        // Save the role permission.
149        KimApiServiceLocator.getRoleService().assignPermissionToRole(newRolePermission.getPermissionId(), newRolePermission.getRoleId());
150    }
151
152    /**
153     * Removes any role members for a given role whose IDs are not listed in a given role member ID set.
154     *
155     * @param roleId The ID of the role containing the role members.
156     * @param existingRoleMemberIds The IDs of the role members that should not be removed.
157     * @throws IllegalArgumentException if roleId is blank or refers to a non-existent role, or if existingRoleMemberIds is null.
158     */
159    static void removeRoleMembers(String roleId, Set<String> existingRoleMemberIds) {
160        if (StringUtils.isBlank(roleId)) {
161            throw new IllegalArgumentException("roleId cannot be blank");
162        } else if (existingRoleMemberIds == null) {
163            throw new IllegalArgumentException("existingRoleMemberIds cannot be null");
164        }
165        RoleService roleUpdateService = KimApiServiceLocator.getRoleService();
166        RoleContract role = KimApiServiceLocator.getRoleService().getRole(roleId);
167        if (role == null) {
168            throw new IllegalArgumentException("Cannot remove role members for role with ID \"" + roleId + "\" because that role does not exist");
169        }
170
171        // Remove any role members whose IDs are not in the set.
172        List<RoleMember> roleMembers = KimApiServiceLocator.getRoleService().findRoleMembers(
173                QueryByCriteria.Builder.fromPredicates(equal("roleId", roleId))).getResults();
174        if (roleMembers != null && !roleMembers.isEmpty()) {
175            for (RoleMemberContract roleMember : roleMembers) {
176                if (!existingRoleMemberIds.contains(roleMember.getId())) {
177                    // If the role member needs to be removed, use the member type code to determine which removal method to call.
178                    MemberType memberType = roleMember.getType();
179                    if (MemberType.PRINCIPAL.equals(memberType)) {
180                        roleUpdateService.removePrincipalFromRole(roleMember.getMemberId(), role.getNamespaceCode(), role.getName(),
181                                (roleMember.getAttributes() != null) ? roleMember.getAttributes() : new HashMap<String, String>());
182                    } else if (MemberType.GROUP.equals(memberType)) {
183                        roleUpdateService.removeGroupFromRole(roleMember.getMemberId(), role.getNamespaceCode(), role.getName(),
184                                (roleMember.getAttributes() != null) ? roleMember.getAttributes() :new HashMap<String, String>());
185                    } else if (MemberType.ROLE.equals(memberType)) {
186                        roleUpdateService.removeRoleFromRole(roleMember.getMemberId(), role.getNamespaceCode(), role.getName(),
187                                (roleMember.getAttributes() != null) ? roleMember.getAttributes() : new HashMap<String, String>());
188                    }
189                }
190            }
191        }
192    }
193
194    /**
195     * Validates a new role's name, namespace, KIM type, and description, and sets the role's ID if the name and namespace match an existing role.
196     */
197    private static void validateAndPrepareRole(RoleXmlDTO newRole) throws UnmarshalException {
198        // Ensure that the role name, role namespace, KIM type, and description have all been specified.
199        if (StringUtils.isBlank(newRole.getRoleName()) || StringUtils.isBlank(newRole.getNamespaceCode())) {
200            throw new UnmarshalException("Cannot create or override a role with a blank name or a blank namespace");
201        } else if (StringUtils.isBlank(newRole.getKimTypeId())) {
202            throw new UnmarshalException("Cannot create or override a role without specikfying a KIM type");
203        } else if (StringUtils.isBlank(newRole.getRoleDescription())) {
204            throw new UnmarshalException("Cannot create or override a role with a blank description");
205        }
206
207        // Attempt to find an existing matching role, and assign its ID to the validated role if it exists.
208        String matchingId = KimApiServiceLocator.getRoleService().getRoleIdByNamespaceCodeAndName(
209                newRole.getNamespaceCode(), newRole.getRoleName());
210        if (StringUtils.isNotBlank(matchingId)) {
211            newRole.setRoleId(matchingId);
212        }
213    }
214
215    /**
216     * Validates a new role member's role ID, role name, and role namespace.
217     */
218    private static void validateRoleIdAndRoleNameForMember(RoleMemberXmlDTO newRoleMember) throws UnmarshalException {
219        // If the "roleMember" tag was not a descendant of a "role" tag, derive and validate its role information accordingly.
220        if (newRoleMember instanceof RoleMemberXmlDTO.OutsideOfRole) {
221            RoleMemberXmlDTO.OutsideOfRole standaloneMember = (RoleMemberXmlDTO.OutsideOfRole) newRoleMember;
222            if (standaloneMember.getRoleNameAndNamespace() != null) {
223                // If a name + namespace combo is given, verify that the combo maps to an existing role.
224                String existingId = KimApiServiceLocator.getRoleService().getRoleIdByNamespaceCodeAndName(
225                        standaloneMember.getRoleNamespaceCode(), standaloneMember.getRoleName());
226                if (StringUtils.isBlank(existingId)) {
227                    throw new UnmarshalException("Cannot create role member for role with name \"" + standaloneMember.getRoleName() + "\" and namespace \"" +
228                            standaloneMember.getRoleNamespaceCode() + "\" because such a role does not exist");
229                }
230
231                // If the role member defines its own role ID, verify that it's the same as the one from the existing role; otherwise, assign the member's role ID.
232                if (StringUtils.isBlank(standaloneMember.getRoleId())) {
233                    standaloneMember.setRoleId(existingId);
234                } else if (!standaloneMember.getRoleId().equals(existingId)) {
235                    throw new UnmarshalException("Cannot create role member for role with ID \"" + standaloneMember.getRoleId() + "\", name \"" +
236                            standaloneMember.getRoleName() + "\", and namespace \"" + standaloneMember.getRoleNamespaceCode() +
237                                    "\" because the existing role with the same name and namespace has an ID of \"" + existingId + "\" instead");
238                }
239            } else if (StringUtils.isBlank(standaloneMember.getRoleId())) {
240                throw new UnmarshalException("Cannot create role member without providing the role ID or role name + namespace that the member belongs to");
241            } else if (KimApiServiceLocator.getRoleService().getRole(standaloneMember.getRoleId()) == null) {
242                throw new UnmarshalException("Cannot create role member for the role with ID \"" + standaloneMember.getRoleId() + "\" because that role does not exist");
243            }
244        }
245
246        // Ensure that a role ID was explicitly defined or was derived from a name + namespace combo.
247        if (StringUtils.isBlank(newRoleMember.getRoleId())) {
248            throw new UnmarshalException("Cannot create role member without providing the role ID or role name + namespace that the member belongs to");
249        }
250    }
251
252    /**
253     * Validates a new role member's member type, member ID, member name, and (if applicable) member namespace code.
254     */
255    private static void validateMemberIdentity(RoleMemberXmlDTO newRoleMember) throws UnmarshalException {
256        // Ensure that sufficient and non-conflicting membership info has been set. (The getMemberTypeCode() method performs such validation.)
257        MemberType memberType = newRoleMember.getMemberType();
258        if (memberType == null) {
259            throw new UnmarshalException("Cannot create a role member with no member principal/group/role identification information specified");
260        }
261
262        // Ensure that a valid member ID was specified, if present.
263        if (StringUtils.isNotBlank(newRoleMember.getMemberId())) {
264            if (MemberType.PRINCIPAL.equals(memberType)) {
265                // If the member is a principal, ensure that the principal exists.
266                if (KimApiServiceLocator.getIdentityService().getPrincipal(newRoleMember.getPrincipalId()) == null) {
267                    throw new UnmarshalException("Cannot create principal role member with principal ID \"" +
268                            newRoleMember.getPrincipalId() + "\" because such a person does not exist");
269                }
270            } else if (MemberType.GROUP.equals(memberType)) {
271                // If the member is a group, ensure that the group exists.
272                if (KimApiServiceLocator.getGroupService().getGroup(newRoleMember.getGroupId()) == null) {
273                    throw new UnmarshalException("Cannot create group role member with group ID \"" +
274                            newRoleMember.getGroupId() + "\" because such a group does not exist");
275                }
276            } else if (MemberType.ROLE.equals(memberType)) {
277                // If the member is another role, ensure that the role exists and that the role is not trying to become a member of itself.
278                if (newRoleMember.getRoleId().equals(newRoleMember.getRoleIdAsMember())) {
279                    throw new UnmarshalException("The role with ID \"" + newRoleMember.getRoleIdAsMember() + "\" cannot be made a member of itself");
280                } else if (KimApiServiceLocator.getRoleService().getRole(newRoleMember.getRoleIdAsMember()) == null) {
281                    throw new UnmarshalException("Cannot use role with ID \"" + newRoleMember.getRoleIdAsMember() +
282                            "\" as a role member because such a role does not exist");
283                }
284            }
285        }
286
287        // Ensure that a valid member name (and namespace, if applicable) was specified, if present.
288        if (StringUtils.isNotBlank(newRoleMember.getMemberName())) {
289            if (MemberType.PRINCIPAL.equals(memberType)) {
290                //If the member is a principal, ensure that the principal exists and does not conflict with any existing principal ID information.
291                PrincipalContract tempPrincipal = KimApiServiceLocator.getIdentityService().getPrincipalByPrincipalName(newRoleMember.getPrincipalName());
292                if (tempPrincipal == null) {
293                    throw new UnmarshalException("Cannot create principal role member with principal name \"" +
294                            newRoleMember.getPrincipalName() + "\" because such a person does not exist");
295                } else if (StringUtils.isBlank(newRoleMember.getPrincipalId())) {
296                    // If no principal ID was given, assign one from the retrieved principal.
297                    newRoleMember.setPrincipalId(tempPrincipal.getPrincipalId());
298                } else if (!newRoleMember.getPrincipalId().equals(tempPrincipal.getPrincipalId())) {
299                    throw new UnmarshalException("Cannot create principal role member with principal ID \"" + newRoleMember.getPrincipalId() +
300                            "\" and principal name \"" + newRoleMember.getPrincipalName() + "\" because the principal with that name has an ID of \"" +
301                                    tempPrincipal.getPrincipalId() + "\" instead");
302                }
303            } else if (MemberType.GROUP.equals(memberType)) {
304                // If the member is a group, ensure that the group exists and does not conflict with any existing group ID information.
305                NameAndNamespacePair groupNameAndNamespace = newRoleMember.getGroupName();
306                GroupContract tempGroup = KimApiServiceLocator.getGroupService().getGroupByNamespaceCodeAndName(
307                        groupNameAndNamespace.getNamespaceCode(), groupNameAndNamespace.getName());
308                if (tempGroup == null) {
309                    throw new UnmarshalException("Cannot create group role member with namespace \"" + groupNameAndNamespace.getNamespaceCode() +
310                            "\" and name \"" + groupNameAndNamespace.getName() + "\" because such a group does not exist");
311                } else if (StringUtils.isBlank(newRoleMember.getGroupId())) {
312                    // If no group ID was given, assign one from the retrieved group.
313                    newRoleMember.setGroupId(tempGroup.getId());
314                } else if (!newRoleMember.getGroupId().equals(tempGroup.getId())) {
315                    throw new UnmarshalException("Cannot create group role member with ID \"" + newRoleMember.getGroupId() + "\", namespace \"" +
316                            groupNameAndNamespace.getNamespaceCode() + "\", and name \"" + groupNameAndNamespace.getName() +
317                                    "\" because the group with that namespace and name has an ID of \"" + tempGroup.getId() + "\" instead");
318                }
319            } else if (MemberType.ROLE.equals(memberType)) {
320                // If the member is another role, ensure that the role exists, does not conflict with any existing role ID information, and is not the member's role.
321                NameAndNamespacePair roleNameAndNamespace = newRoleMember.getRoleNameAsMember();
322                RoleContract tempRole = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName(
323                        roleNameAndNamespace.getNamespaceCode(), roleNameAndNamespace.getName());
324                if (tempRole == null) {
325                    throw new UnmarshalException("Cannot use role with namespace \"" + roleNameAndNamespace.getNamespaceCode() +
326                            "\" and name \"" + roleNameAndNamespace.getName() + "\" as a role member because such a role does not exist");
327                } else if (newRoleMember.getRoleId().equals(tempRole.getId())) {
328                    throw new UnmarshalException("The role with namespace \"" + roleNameAndNamespace.getNamespaceCode() +
329                            "\" and name \"" + roleNameAndNamespace.getName() + "\" cannot be made a member of itself");
330                } else if (StringUtils.isBlank(newRoleMember.getRoleId())) {
331                    // If no role ID was given, assign one from the retrieved role.
332                    newRoleMember.setRoleIdAsMember(tempRole.getId());
333                } else if (!newRoleMember.getRoleId().equals(tempRole.getId())) {
334                    throw new RuntimeException("Cannot use role with ID \"" + newRoleMember.getRoleId() + "\", namespace \"" +
335                            roleNameAndNamespace.getNamespaceCode() + "\", and name \"" + roleNameAndNamespace.getName() +
336                                    "\" as a role member because the role with that namespace and name has an ID of \"" +
337                                            tempRole.getId() + "\" instead");
338                }
339            }
340        }
341
342        // Ensure that a member ID was either explicitly defined or was derived from the member name (and namespace, if applicable).
343        if (StringUtils.isBlank(newRoleMember.getMemberId())) {
344            throw new RuntimeException("Cannot create a role member with no member principal/group/role identification information specified");
345        }
346
347    }
348
349    /**
350     * Validates a role permission's role and permission identification information, and assigns its role ID if needed.
351     */
352    private static void validateAndPrepareRolePermission(RolePermissionXmlDTO newRolePermission) throws UnmarshalException {
353
354        // If this is a standalone role permission, derive and validate its role information accordingly.
355        if (newRolePermission instanceof RolePermissionXmlDTO.OutsideOfRole) {
356            RolePermissionXmlDTO.OutsideOfRole standaloneRolePerm = (RolePermissionXmlDTO.OutsideOfRole) newRolePermission;
357            if (standaloneRolePerm.getRoleNameAndNamespace() != null) {
358                // If a role name + namespace is given, assign or validate the role ID accordingly.
359                String tempRoleId = KimApiServiceLocator.getRoleService().getRoleIdByNamespaceCodeAndName(
360                        standaloneRolePerm.getRoleNamespaceCode(), standaloneRolePerm.getRoleName());
361                if (StringUtils.isBlank(tempRoleId)) {
362                    throw new UnmarshalException("Cannot assign permission to role with namespace \"" + standaloneRolePerm.getRoleNamespaceCode() +
363                            "\" and name \"" + standaloneRolePerm.getRoleName() + "\" because that role does not exist");
364                } else if (StringUtils.isBlank(standaloneRolePerm.getRoleId())) {
365                    // If no role ID was given, assign one from the retrieved role.
366                    standaloneRolePerm.setRoleId(standaloneRolePerm.getRoleId());
367                } else if (!standaloneRolePerm.getRoleId().equals(tempRoleId)) {
368                    throw new UnmarshalException("Cannot assign permission to role with ID \"" + standaloneRolePerm.getRoleId() + "\", namespace \"" +
369                            standaloneRolePerm.getRoleNamespaceCode() + "\", and name \"" + standaloneRolePerm.getRoleName() +
370                                    "\" because the existing role with that name and namespace has an ID of \"" + tempRoleId + "\" instead");
371                }
372            } else if (StringUtils.isBlank(standaloneRolePerm.getRoleId())) {
373                throw new UnmarshalException(
374                        "Cannot assign permission to role without providing the role ID or role name + namespace that the permission is assigned to");
375            } else if (KimApiServiceLocator.getRoleService().getRole(standaloneRolePerm.getRoleId()) == null) {
376                throw new UnmarshalException("Cannot assign permission to role with ID \"" + standaloneRolePerm.getRoleId() +
377                        "\" because that role does not exist");
378            }
379        }
380
381        // Ensure that a role ID was explicitly defined or was derived from a name + namespace combo.
382        if (StringUtils.isBlank(newRolePermission.getRoleId())) {
383            throw new UnmarshalException("Cannot assign permission to role without providing the role ID or role name + namespace that the permission is assigned to");
384        }
385
386        // If the permission is being identified by name and namespace, derive or validate its permission ID accordingly.
387        if (newRolePermission.getPermissionNameAndNamespace() != null) {
388            PermissionContract permission = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName(
389                    newRolePermission.getPermissionNamespaceCode(), newRolePermission.getPermissionName());
390            if (permission == null) {
391                throw new UnmarshalException("Cannot get role assigned to permission with namespace \"" + newRolePermission.getPermissionNamespaceCode() +
392                        "\" and name \"" + newRolePermission.getPermissionName() + "\" because that permission does not exist");
393            } else if (StringUtils.isBlank(newRolePermission.getPermissionId())) {
394                // If no permission ID was given, assign one from the retrieved permission.
395                newRolePermission.setPermissionId(permission.getId());
396            } else if (!newRolePermission.getPermissionId().equals(permission.getId())) {
397                throw new UnmarshalException("Cannot get role assigned to permission with ID \"" + newRolePermission.getPermissionId() + "\", namespace \"" +
398                        newRolePermission.getPermissionNamespaceCode() + "\", and name \"" + newRolePermission.getPermissionName() +
399                                "\" because the existing permission with that name and namespace has an ID of \"" + permission.getId() + "\" instead");
400            }
401        } else if (StringUtils.isBlank(newRolePermission.getPermissionId())) {
402            throw new UnmarshalException("Cannot assign permission to role without specifying the ID or name and namespace of the permission to assign");
403        } else if (KimApiServiceLocator.getPermissionService().getPermission(newRolePermission.getPermissionId()) == null) {
404            throw new UnmarshalException("Cannot get role assigned to permission with ID \"" + newRolePermission.getPermissionId() +
405                    "\" because that permission does not exist");
406        }
407    }
408
409}