OrganizationMemberService.java
package edu.ucsb.cs156.frontiers.services;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.entities.CourseStaff;
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
import edu.ucsb.cs156.frontiers.enums.OrgStatus;
import edu.ucsb.cs156.frontiers.models.OrgMember;
import edu.ucsb.cs156.frontiers.repositories.RosterStudentRepository;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Service
public class OrganizationMemberService {
private final JwtService jwtService;
private final ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private final RosterStudentRepository rosterStudentRepository;
public OrganizationMemberService(
JwtService jwtService,
ObjectMapper objectMapper,
RestTemplateBuilder builder,
RosterStudentRepository rosterStudentRepository) {
this.jwtService = jwtService;
this.objectMapper = objectMapper;
this.rosterStudentRepository = rosterStudentRepository;
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.restTemplate = builder.build();
}
/**
* This endpoint returns the list of **members**, not admins for the organization. This is so that
* the roles are known for the return values.
*/
public Iterable<OrgMember> getOrganizationMembers(Course course)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
String ENDPOINT = "https://api.github.com/orgs/" + course.getOrgName() + "/members?role=member";
return getOrganizationMembersWithRole(course, ENDPOINT);
}
/**
* This endpoint returns the list of **admins** for the organization. This is so that the roles
* are known for the return values.
*/
public Iterable<OrgMember> getOrganizationAdmins(Course course)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
String ENDPOINT = "https://api.github.com/orgs/" + course.getOrgName() + "/members?role=admin";
return getOrganizationMembersWithRole(course, ENDPOINT);
}
/**
* This endpoint returns the list of users who have been **invited** to the organization but have
* not yet accepted.
*/
public Iterable<OrgMember> getOrganizationInvitees(Course course)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
String ENDPOINT = "https://api.github.com/orgs/" + course.getOrgName() + "/invitations";
return getOrganizationMembersWithRole(course, ENDPOINT);
}
private Iterable<OrgMember> getOrganizationMembersWithRole(Course course, String ENDPOINT)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
// happily stolen directly from GitHub:
// https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28
Pattern pattern = Pattern.compile("(?<=<)([\\S]*)(?=>; rel=\"next\")");
String token = jwtService.getInstallationToken(course);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response =
restTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, String.class);
List<String> responseLinks = response.getHeaders().getOrEmpty("link");
List<OrgMember> orgMembers = new ArrayList<>();
while (!responseLinks.isEmpty() && responseLinks.getFirst().contains("next")) {
orgMembers.addAll(
objectMapper.convertValue(
objectMapper.readTree(response.getBody()), new TypeReference<List<OrgMember>>() {}));
Matcher matcher = pattern.matcher(responseLinks.getFirst());
matcher.find();
response = restTemplate.exchange(matcher.group(0), HttpMethod.GET, entity, String.class);
responseLinks = response.getHeaders().getOrEmpty("link");
}
orgMembers.addAll(
objectMapper.convertValue(
objectMapper.readTree(response.getBody()), new TypeReference<List<OrgMember>>() {}));
return orgMembers;
}
public OrgStatus inviteOrganizationMember(RosterStudent student)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
Course course = student.getCourse();
return inviteMember(student.getGithubId(), course, "direct_member", student.getGithubLogin());
}
public OrgStatus inviteOrganizationOwner(CourseStaff staff)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
Course course = staff.getCourse();
return inviteMember(staff.getGithubId(), course, "admin", staff.getGithubLogin());
}
private OrgStatus inviteMember(int githubId, Course course, String role, String githubLogin)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String ENDPOINT = "https://api.github.com/orgs/" + course.getOrgName() + "/invitations";
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
Map<String, Object> body = new HashMap<>();
body.put("invitee_id", githubId);
body.put("role", role);
String bodyAsJson = objectMapper.writeValueAsString(body);
HttpEntity<String> entity = new HttpEntity<>(bodyAsJson, headers);
try {
restTemplate.exchange(ENDPOINT, HttpMethod.POST, entity, String.class);
} catch (HttpClientErrorException e) {
return getMemberStatus(githubLogin, course);
}
return OrgStatus.INVITED;
}
private OrgStatus getMemberStatus(String githubLogin, Course course)
throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException {
String ENDPOINT =
"https://api.github.com/orgs/" + course.getOrgName() + "/memberships/" + githubLogin;
HttpHeaders headers = new HttpHeaders();
String token = jwtService.getInstallationToken(course);
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
try {
ResponseEntity<String> response =
restTemplate.exchange(ENDPOINT, HttpMethod.GET, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
if (responseJson.get("role").asText().equalsIgnoreCase("admin")) {
return OrgStatus.OWNER;
} else if (responseJson.get("role").asText().equalsIgnoreCase("member")) {
return OrgStatus.MEMBER;
} else {
log.warn(
"Unexpected role {} used in course {}",
responseJson.get("role").asText(),
course.getCourseName());
return OrgStatus.JOINCOURSE;
}
} catch (HttpClientErrorException e) {
log.warn("Error while trying to get member status: {}", e.getMessage());
return OrgStatus.JOINCOURSE;
}
}
/**
* Removes a member from an organization.
*
* @param student The roster student to remove from the organization
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
* @throws JsonProcessingException if there is an error processing JSON
* @throws IllegalArgumentException if student has no GitHub login or course has no linked
* organization
* @throws Exception if there is an error removing the student from the organization
*/
public void removeOrganizationMember(RosterStudent student)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
if (student.getGithubLogin() == null) {
throw new IllegalArgumentException(
"Cannot remove student from organization: GitHub login is null");
}
Course course = student.getCourse();
if (course.getOrgName() == null || course.getInstallationId() == null) {
throw new IllegalArgumentException(
"Cannot remove student from organization: Course has no linked organization");
}
removeOrganizationMember(
course.getOrgName(), student.getGithubLogin(), jwtService.getInstallationToken(course));
}
/**
* Removes a member from an organization.
*
* @param staffMember The staff member to remove from the organization
* @throws NoSuchAlgorithmException if there is an algorithm error
* @throws InvalidKeySpecException if there is a key specification error
* @throws JsonProcessingException if there is an error processing JSON
* @throws IllegalArgumentException if student has no GitHub login or course has no linked
* organization
* @throws Exception if there is an error removing the student from the organization
*/
public void removeOrganizationMember(CourseStaff staffMember)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
if (staffMember.getGithubLogin() == null) {
throw new IllegalArgumentException(
"Cannot remove staff member from organization: GitHub login is null");
}
Course course = staffMember.getCourse();
if (course.getOrgName() == null || course.getInstallationId() == null) {
throw new IllegalArgumentException(
"Cannot remove staff member from organization: Course has no linked organization");
}
removeOrganizationMember(
course.getOrgName(), staffMember.getGithubLogin(), jwtService.getInstallationToken(course));
}
/**
* Remove member from organization
*
* @param orgName
* @param githubLogin
* @param token
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws JsonProcessingException
*/
public void removeOrganizationMember(String orgName, String githubLogin, String token)
throws NoSuchAlgorithmException, InvalidKeySpecException, JsonProcessingException {
String ENDPOINT = "https://api.github.com/orgs/" + orgName + "/members/" + githubLogin;
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
restTemplate.exchange(ENDPOINT, HttpMethod.DELETE, entity, String.class);
log.info("Successfully removed student {} from organization {}", githubLogin, orgName);
}
}