All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.iu.uits.lms.provisioning.service.EnrollmentProvisioning Maven / Gradle / Ivy

package edu.iu.uits.lms.provisioning.service;

/*-
 * #%L
 * lms-lti-3rdpartyprovisioning
 * %%
 * Copyright (C) 2015 - 2022 Indiana University
 * %%
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. Neither the name of the Indiana University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software without
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

import edu.iu.uits.lms.canvas.model.Account;
import edu.iu.uits.lms.canvas.model.Course;
import edu.iu.uits.lms.canvas.model.User;
import edu.iu.uits.lms.canvas.services.AccountService;
import edu.iu.uits.lms.canvas.services.CourseService;
import edu.iu.uits.lms.canvas.services.UserService;
import edu.iu.uits.lms.iuonly.services.SisServiceImpl;
import edu.iu.uits.lms.provisioning.model.ImsUser;
import edu.iu.uits.lms.provisioning.model.content.FileContent;
import edu.iu.uits.lms.provisioning.model.content.StringArrayFileContent;
import edu.iu.uits.lms.provisioning.repository.ImsUserRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Code for provisioning users into a Canvas enrollment
 */
@Slf4j
@Service
public class EnrollmentProvisioning {

    public static final String DELETED = "deleted";
    public static final String ACTIVE = "active";

    @Autowired
    private UserService userService = null;

    @Autowired
    private GuestAccountService guestAccountService = null;

    @Autowired
    private CsvService csvService = null;

    @Autowired
    private ImsUserRepository imsUserRepository = null;

    @Autowired
    private AccountService accountService;

    @Autowired
    private CourseService courseService;

    @Autowired
    private SisServiceImpl sisService;

    /**
     * Pass in a path to a csv file and a department code and this validate the data and send enrollments to Canvas
     * @param fileToProcess List of files to process
     * @param allowSis Flag indicating if sis enrollments are allowed
     * @param authorizedAccounts List of authorized accounts
     * @param overrideRestrictions Flag indicating of restrictions are to be overridden
     * @return List of ProvisioningResult objects
     */
    public List processEnrollments(Collection fileToProcess, boolean allowSis, List authorizedAccounts, boolean overrideRestrictions) {
        List prs = new ArrayList<>();
        for (FileContent file : fileToProcess) {
            StringBuilder emailMessage = new StringBuilder();

            StringBuilder finalMessage = new StringBuilder(file.getFileName() + ":\r\n");

            // process input files and transform into what will be sent off
            List outputData = processInputFiles((StringArrayFileContent)file, emailMessage, allowSis, authorizedAccounts, overrideRestrictions);

            // Create csv file to send to Canvas
            String[] enrollmentsHeader = CsvService.ENROLLMENTS_HEADER_SECTION_LIMIT.split(",");

            byte[] fileBytes = null;
            boolean fileException = false;
            try {
                fileBytes = csvService.writeCsvToBytes(outputData, enrollmentsHeader);
                finalMessage.append(emailMessage);
            } catch (IOException e) {
                log.error("Error generating csv", e);
                finalMessage.append("\tThere were errors when generating the CSV file to send to Canvas\r\n");
                fileException = true;
            }

            prs.add(new ProvisioningResult(finalMessage, new ProvisioningResult.FileObject(file.getFileName(), fileBytes), fileException));
        }
        return prs;
    }

    /**
     * @param fileToProcess
     * @param emailMessage
     * @return
     */
    private List processInputFiles(StringArrayFileContent fileToProcess, StringBuilder emailMessage, boolean allowSisEnrollments, List authorizedAccounts, boolean overrideRestrictions) {
        List stringArray = new ArrayList<>();

        int rejected = 0;
        int success = 0;
        int rowCounter = 0;

        // read individual files line by line
        List  fileContents = fileToProcess.getContents();
        Map sisCourses = new HashMap<>();
        Map sisSections = new HashMap<>();
        Map accountChecksMap = new HashMap<>();
        Map courseAndAccountTrackerMap = new HashMap<>();

        for (String[] lineContentArray : fileContents) {
            // increment the counter here and not several places
            rowCounter++;

            int lineLength = lineContentArray.length;
            if (lineLength == 5 || lineLength == 6) {
                // check to see if this is a header line
                // if it's a header row, we want to ignore it and move on
                String[] enrollmentsHeader = CsvService.ENROLLMENTS_HEADER.split(",");
                String[] enrollmentsHeaderSectionLimit = CsvService.ENROLLMENTS_HEADER_SECTION_LIMIT.split(",");
                if (Arrays.equals(lineContentArray,enrollmentsHeader) || Arrays.equals(lineContentArray,enrollmentsHeaderSectionLimit)) {
                    continue;
                }

                String courseId = lineContentArray[0];
                String emailOrUserId = lineContentArray[1];
                String role = lineContentArray[2];
                String sectionId = lineContentArray[3];
                String status = lineContentArray[4];

                // if overrideRestrictions OR allowSis is true, skip the SIS checks
                boolean doSisCheck = !overrideRestrictions && !allowSisEnrollments;

                // check existing maps to see if we've looked up this info previously and
                if (doSisCheck) {
                    if (sisCourses.containsKey(courseId)) {
                        if (sisCourses.get(courseId)) {
                            // confirmed course is 'true' in the map, so skip the sis course lookup later
                            doSisCheck = false;
                        } else {
                            // confirmed course is 'false', so skip this line since we know it's not ok to use
                            log.warn("Skipped " + emailOrUserId + " because it is in a SIS course and we already checked.");
                            rejected++;
                            emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized for SIS changes.\r\n");
                            continue;
                        }
                    }

                    if (sisSections.containsKey(sectionId)) {
                        if (sisSections.get(sectionId)) {
                            // confirmed section is 'true' in the map, so skip the sis section lookup later
                            doSisCheck = false;
                        } else {
                            // confirmed section is 'false', so skip this line since we know it's not ok to use
                            log.warn("Skipped " + emailOrUserId + " because it is in a SIS section and we already checked.");
                            rejected++;
                            emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized for SIS changes.\r\n");
                            continue;
                        }
                    }
                }

                // if user does not have overrideRestrictions, allowSis, or the course/sections have not been looked up yet, do the SIS checks
                if (doSisCheck) {
                    // look up for SIS stuff
                    if (sisService.isLegitSisCourse(courseId)) {
                        log.warn("Skipped " + emailOrUserId + " because it is in a SIS course and user did not have SIS permission.");
                        rejected++;
                        emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized for SIS changes.\r\n");
                        sisCourses.put(courseId, false);
                        continue;
                    }

                    // look up for SIS stuff
                    if (sisService.isLegitSisCourse(sectionId)) {
                        log.warn("Skipped " + emailOrUserId + " because it is in a SIS section and user did not have SIS permission.");
                        rejected++;
                        emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized for SIS changes.\r\n");
                        sisSections.put(sectionId, false);
                        continue;
                    }
                }

                // if we made it here, it passed the SIS checks. Add it to the lists
                sisCourses.put(courseId, true);
                sisSections.put(sectionId, true);

                boolean isAccountAuthorized = false;
                boolean accountPreviouslyAuthorized = false;

                // check existing maps to see if we've looked up this info previously and deal with it as appropriate
                if (accountChecksMap.containsKey(courseId)) {
                    if (accountChecksMap.get(courseId)) {
                        // we've checked this course's account before and verified, so let's set accountPreviouslyAuthorized to true to bypass the accounts check
                        accountPreviouslyAuthorized = true;
                    } else {
                        // account for course 'false', so skip this line since we know it's not ok to use
                        String accountName = courseAndAccountTrackerMap.get(courseId);
                        log.debug("Skipped " + emailOrUserId + " because we know they're not authorized from a previous lookup.");
                        emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized to work in " + accountName + " subaccount.\r\n");
                        rejected++;
                        continue;
                    }
                }

                // if we made it here, this is a first time account lookup
                if (overrideRestrictions || accountPreviouslyAuthorized) {
                    isAccountAuthorized = true;
                } else if (authorizedAccounts.contains("ALL")) {
                    isAccountAuthorized = true;
                } else {
                    Course course = courseService.getCourse("sis_course_id:" + courseId);
                    if (course == null) {
                        // no course exists, so assuming new entry and letting Canvas deal with it
                        isAccountAuthorized = true;
                        accountChecksMap.put(courseId, true);
                    } else {
                        Account courseAccount = accountService.getAccount(course.getAccountId());
                        if (authorizedAccounts.contains(courseAccount.getName())) {
                            isAccountAuthorized = true;
                            accountChecksMap.put(courseId, true);
                        } else {
                            // course exists, so do checks to see the user is allowed in the node
                            String accountId = course.getAccountId();
                            List parentAccountNames = accountService.getParentAccounts(accountId)
                                    .stream().map(parentNames -> parentNames.getName()).collect(Collectors.toList());

                            for (String authorizedAccount : authorizedAccounts) {
                                if (parentAccountNames.contains(authorizedAccount)) {
                                    isAccountAuthorized = true;
                                    accountChecksMap.put(courseId, true);
                                    break;
                                }
                            }

                            // if we made it to here, this course does not have an authorized account. Add to map for check in future lines.
                            if (!isAccountAuthorized) {
                                accountChecksMap.put(courseId, false);
                                courseAndAccountTrackerMap.put(courseId, courseAccount.getName());
                            }
                        }
                    }
                }

                if (isAccountAuthorized) {
                    // make sure the first object is an email address
                    log.info("Check for a valid email address");
                    if (EmailValidator.getInstance().isValid(emailOrUserId)) {
                        // default to true, unless they specified or is a teacher
                        String sectionLimit = "true";
                        if (lineContentArray.length==6 && !lineContentArray[5].isEmpty()) {
                            if (lineContentArray[5].equalsIgnoreCase("true") || lineContentArray[5].equalsIgnoreCase("false")) {
                                sectionLimit = lineContentArray[5].toLowerCase();
                            }
                        } else if ("teacher".equals(role)) {
                            // extremely unlikely you'd make a new, guest account a teacher, but here it is anyway!
                            sectionLimit = "false";
                        }

                        String[] lineToRewrite = {courseId,emailOrUserId,role,sectionId,status,sectionLimit};
                        // add the object to our list of users to send to Canvas
                        stringArray.add(lineToRewrite);
                        success++;
                    } else {
                        // Not a valid email address, so treat it as a normal IU account or emplId
                        // default to true, unless csv specifies or is a teacher
                        String sectionLimit = "true";
                        if (lineContentArray.length==6 && !lineContentArray[5].isEmpty()) {
                            if (lineContentArray[5].equalsIgnoreCase("true") || lineContentArray[5].equalsIgnoreCase("false")) {
                                sectionLimit = lineContentArray[5].toLowerCase();
                            }
                        } else if ("teacher".equals(role)) {
                            // teacher role and was not specified in the csv
                            sectionLimit = "false";
                        }

                        boolean isEmplId = emailOrUserId.matches("[0-9]+");
                        String emplId = "";

                        if (isEmplId) {
                            // Put the error checking on Canvas!
                            emplId = emailOrUserId;
                        } else {
                            // Look up the userId from the IMS feed to get the 10 digit id
                            ImsUser imsUser = imsUserRepository.findByLoginId(emailOrUserId);
                            if (imsUser != null) {
                                emplId = imsUser.getUserId();
                            }

                            // if there is no emplId for a username lookup in IMS, try Canvas
                            if (emplId == null || "".equals(emplId)) {
                                // assure emplId is empty to make a status check easier later
                                emplId = "";

                                // regular username, so try this
                                User user = userService.getUserBySisLoginId(emailOrUserId);
                                if (user != null && user.getSisUserId() != null) {
                                    emplId = user.getSisUserId();
                                }

                                if (emplId.isEmpty()) {
                                    log.warn("Could not find emplId for: '" + emailOrUserId + "'");
                                    rejected++;
                                    emailMessage.append("\tLine " + rowCounter + ": no emplId found for " + emailOrUserId + "\r\n");
                                    continue;
                                }
                            }
                        }

                        String[] lineToRewrite = {courseId,emplId,role,sectionId,status,sectionLimit};

                        // add the object to our list of users to send to Canvas
                        stringArray.add(lineToRewrite);
                        success++;
                    }
                } else {
                    String accountName = courseAndAccountTrackerMap.get(courseId);
                    log.debug("Skipped " + emailOrUserId + " because provisioning user is not authorized to provision to this account.");
                    emailMessage.append("\tLine " + rowCounter + ": Enrollment for " + emailOrUserId + " rejected. Not authorized to work in " + accountName + " subaccount.\r\n");
                    rejected++;
                    continue;
                }
            }
        }
        int total = rejected + success;
        emailMessage.append("\tEnrollments rejected: " + rejected + "\r\n");
        emailMessage.append("\tEnrollments sent to Canvas: " + success + "\r\n");
        emailMessage.append("\tTotal enrollments processed: " + total + "\r\n");

        return stringArray;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy