diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java index ceb321fbbf..93d3b264f0 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java @@ -19,6 +19,9 @@ */ package org.apache.airavata.research.service.config; +import static org.apache.airavata.research.service.enums.AuthorRoleEnum.PRIMARY; +import static org.apache.airavata.research.service.enums.StateEnum.ACTIVE; + import java.util.HashSet; import java.util.Set; import org.apache.airavata.research.service.enums.PrivacyEnum; @@ -26,6 +29,7 @@ import org.apache.airavata.research.service.model.entity.DatasetResource; import org.apache.airavata.research.service.model.entity.Project; import org.apache.airavata.research.service.model.entity.RepositoryResource; +import org.apache.airavata.research.service.model.entity.ResourceAuthor; import org.apache.airavata.research.service.model.entity.Tag; import org.apache.airavata.research.service.model.repo.ProjectRepository; import org.apache.airavata.research.service.model.repo.ResourceRepository; @@ -54,7 +58,7 @@ public DevDataInitializer( } private void createProject( - String name, String description, String repoUrl, String datasetUrl, String[] tags, String user) { + String name, String description, String repoUrl, String datasetUrl, String[] tags, Set authors) { Set tagSet = new HashSet<>(); for (String tag : tags) { Tag t = tagRepository.findByValue(tag); @@ -68,12 +72,6 @@ private void createProject( } } - Set authors = new HashSet<>() { - { - add(user); - } - }; - RepositoryResource repo = new RepositoryResource(); repo.setName(name); repo.setDescription(description); @@ -82,7 +80,16 @@ private void createProject( repo.setStatus(StatusEnum.VERIFIED); repo.setPrivacy(PrivacyEnum.PUBLIC); repo.setTags(tagSet); - repo.setAuthors(authors); + repo.setState(ACTIVE); + Set repoResourceAuthors = new HashSet<>(); + for (String author : authors) { + ResourceAuthor a = new ResourceAuthor(); + a.setResource(repo); + a.setRole(PRIMARY); + a.setAuthorId(author); + repoResourceAuthors.add(a); + } + repo.setAuthors(repoResourceAuthors); repo = resourceRepository.save(repo); DatasetResource dataset = new DatasetResource(); @@ -93,14 +100,24 @@ private void createProject( dataset.setStatus(StatusEnum.VERIFIED); dataset.setPrivacy(PrivacyEnum.PUBLIC); dataset.setTags(tagSet); - dataset.setAuthors(authors); + dataset.setState(ACTIVE); + Set datasetResourceAuthors = new HashSet<>(); + for (String author : authors) { + ResourceAuthor a = new ResourceAuthor(); + a.setResource(repo); + a.setRole(PRIMARY); + a.setAuthorId(author); + datasetResourceAuthors.add(a); + } + dataset.setAuthors(datasetResourceAuthors); dataset = resourceRepository.save(dataset); Project project = new Project(); project.setRepositoryResource(repo); project.getDatasetResources().add(dataset); project.setName(name); - project.setOwnerId(user); + project.setState(ACTIVE); + project.setOwnerId(String.join(", ", authors.stream().toString())); projectRepository.save(project); System.out.println("Initialized Project with id: " + project.getId()); @@ -108,18 +125,21 @@ private void createProject( @Override public void run(String... args) { + System.out.println("HRSDSF"); if (projectRepository.count() > 0) { System.out.println("Dev data already initialized. Skipping initialization."); return; } + System.out.println("Initializing dev data..."); + createProject( "Bio-realistic multiscale simulations of cortical circuits", "Running the AllenAI V1 model, with thalamacortical (LGN) and background (BKG) inputs", "https://github.com/cyber-shuttle/allenai-v1", "allenai-v1", new String[] {"neurodata25", "allenai", "visual_cortex"}, - "Anton Arkhipov, Laura Green"); + Set.of("Anton Arkhipov", "Laura Green")); createProject( "Apache Cerebrum", @@ -127,7 +147,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/airavata-cerebrum", "apache-airavata-cerebrum", new String[] {"neurodata25", "apache", "cerebrum"}, - "Sriram Chockalingam"); + Set.of("Sriram Chockalingam")); createProject( "Spatio-temporal dynamics of sleep in large-scale brain models", @@ -135,7 +155,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/whole-brain-public", "bazhlab-whole-brain", new String[] {"neurodata25", "bazhlab", "whole-brain"}, - "Maxim Bazhenov, Gabriela Navas Zuloaga"); + Set.of("Maxim Bazhenov", "Gabriela Navas Zuloaga")); createProject( "Biologically Constrained RNNs", @@ -143,7 +163,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/biologicalRNNs", "hchoilab-biologicalRNNs", new String[] {"neurodata25", "hchoilab", "biological-rnn"}, - "Hannah Choi, Aishwarya Balwani"); + Set.of("Hannah Choi", "Aishwarya Balwani")); createProject( "One-hot Generalized Linear Model for Switching Brain State Discovery", @@ -151,7 +171,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/onehot-hmmglm", "brainml-onehot-hmmglm", new String[] {"neurodata25", "brainml", "hmm-glm"}, - "Anqi Wu, Chengrui Li"); + Set.of("Anqi Wu", "Chengrui Li")); createProject( "Scaling up neural data analysis with torch_brain and temporaldata", @@ -159,7 +179,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/neurodata25_torchbrain_notebooks", "nerdslab-neurodata25", new String[] {"neurodata25", "nerdslab", "torch_brain", "temporaldata"}, - "Eva Dyer, Vinam Arora, Mahato Shivashriganesh"); + Set.of("Eva Dyer, Vinam Arora", "Mahato Shivashriganesh")); createProject( "Bridge the Gap between the Structure and Function in the Brain", @@ -167,7 +187,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/neuroaihub-netformer", "neuroaihub-netformer", new String[] {"neurodata25", "neuroaihub", "netformer"}, - "Lu Mi"); + Set.of("Lu Mi")); createProject( "Computing with Neural Oscillators", @@ -175,7 +195,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/imamlab-neural-oscillators", "imamlab-neurodata25", new String[] {"neurodata25", "imamlab", "neural-oscillators"}, - "Nabil Imam, Nand Chandravadia"); + Set.of("Nabil Imam, Nand Chandravadia")); createProject( "Getting started with Cybershuttle", @@ -183,7 +203,7 @@ public void run(String... args) { "https://github.com/cyber-shuttle/cybershuttle-reference", "cybershuttle-reference", new String[] {"cybershuttle", "apache-airavata", "reference"}, - "Suresh Marru"); + Set.of("Suresh Marru")); createProject( "Malicious URL Detector", @@ -191,7 +211,7 @@ public void run(String... args) { "https://github.com/airavata-courses/malicious-url-detector", "airavata-courses-malicious-url-detector", new String[] {"airavata-courses", "spring-2025"}, - "Krish Katariya, Jesse Gong, Shreyas Arisa, Devin Fromond"); + Set.of("Krish Katariya", "Jesse Gong", "Shreyas Arisa", "Devin Fromond")); createProject( "Deepseek Remote Execution", @@ -199,7 +219,7 @@ public void run(String... args) { "https://github.com/ZhenmeiOng/proj2-llama", "airavata-courses-deepseek-chat", new String[] {"airavata-courses", "spring-2025", "llm"}, - "Yashkaran Chauhan, Zhenmei Ong, Varenya Amagowni"); + Set.of("Yashkaran Chauhan", "Zhenmei Ong", "Varenya Amagowni")); createProject( "Fast Chat", @@ -207,6 +227,6 @@ public void run(String... args) { "https://github.com/riccog/cybershuttle", "airavata-courses-fast-chat", new String[] {"airavata-courses", "spring-2025"}, - "Ricco Goss, Mason Graham, Talam, Ruchira"); + Set.of("Ricco Goss", "Mason Graham", "Talam", "Ruchira")); } } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/AdminController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/AdminController.java new file mode 100644 index 0000000000..74c9d005dc --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/AdminController.java @@ -0,0 +1,70 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.handlers.AdminHandler; +import org.apache.airavata.research.service.handlers.ResourceHandler; +import org.apache.airavata.research.service.model.entity.Resource; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/rf/admin") +@Tag(name = "Admin Controls ", description = "Operations performable by Cybershuttle admins") +public class AdminController { + + private final ResourceHandler resourceHandler; + private final AdminHandler adminHandler; + + public AdminController(ResourceHandler resourceHandler, AdminHandler adminHandler) { + this.resourceHandler = resourceHandler; + this.adminHandler = adminHandler; + } + + @PostMapping("/resources/{id}/verify") + @Operation(summary = "Verify a resource") + public ResponseEntity verifyResource(@PathVariable(value = "id") String id) { + Resource resource = resourceHandler.getResourceById(id); + return ResponseEntity.ok(adminHandler.verifyResource(resource)); + } + + @PostMapping("/resources/{id}/reject") + @Operation(summary = "Verify a resource") + public ResponseEntity rejectResource( + @PathVariable(value = "id") String id, @RequestBody String rejectionMessage) { + Resource resource = resourceHandler.getResourceById(id); + return ResponseEntity.ok(adminHandler.rejectResource(resource, rejectionMessage)); + } + + @GetMapping("/resources/pending") + @Operation(summary = "Get all pending verification resources") + public ResponseEntity> getPendingResources() { + return ResponseEntity.ok(resourceHandler.getAllResourcesWithStatus(List.of(StatusEnum.PENDING))); + } +} diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java index f02cf7cac5..65469e54c5 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java @@ -35,6 +35,7 @@ import org.apache.airavata.research.service.model.entity.Project; import org.apache.airavata.research.service.model.entity.RepositoryResource; import org.apache.airavata.research.service.model.entity.Resource; +import org.apache.airavata.research.service.model.entity.ResourceVerificationActivity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -167,6 +168,19 @@ public ResponseEntity> getProjectsFromResourceId(@PathVariable(val return ResponseEntity.ok(projects); } + @Operation(summary = "Submit a resource for verification") + @PostMapping(value = "/{id}/verify") + public ResponseEntity submitResourceForVerification(@PathVariable(value = "id") String id) { + return ResponseEntity.ok(resourceHandler.submitResourceForVerification(id)); + } + + @Operation(summary = "Get verification activities for a resource") + @GetMapping(value = "/{id}/verification-activities") + public ResponseEntity> getResourceVerificationActivities( + @PathVariable(value = "id") String id) { + return ResponseEntity.ok(resourceHandler.getResourceVerificationActivities(id)); + } + @Operation(summary = "Star/unstar a resource") @PostMapping(value = "/{id}/star") public ResponseEntity starOrUnstarResource(@PathVariable(value = "id") String id) { diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/CreateResourceRequest.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/CreateResourceRequest.java index b3898febbe..c8b056640c 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/CreateResourceRequest.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/dto/CreateResourceRequest.java @@ -21,6 +21,7 @@ import java.util.Set; import org.apache.airavata.research.service.enums.PrivacyEnum; +import org.apache.airavata.research.service.model.entity.ResourceAuthor; public class CreateResourceRequest { @@ -28,7 +29,7 @@ public class CreateResourceRequest { public String description; public String headerImage; Set tags; - Set authors; + Set authors; PrivacyEnum privacy; public PrivacyEnum getPrivacy() { @@ -39,11 +40,11 @@ public void setPrivacy(PrivacyEnum privacy) { this.privacy = privacy; } - public Set getAuthors() { + public Set getAuthors() { return authors; } - public void setAuthors(Set authors) { + public void setAuthors(Set authors) { this.authors = authors; } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/AuthorRoleEnum.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/AuthorRoleEnum.java new file mode 100644 index 0000000000..f2609fe135 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/AuthorRoleEnum.java @@ -0,0 +1,28 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.enums; + +public enum AuthorRoleEnum { + PRIMARY, + SECONDARY, + TERTIARY, + QUATERNARY, + QUINARY +} diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/AdminHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/AdminHandler.java new file mode 100644 index 0000000000..1773446042 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/AdminHandler.java @@ -0,0 +1,90 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.handlers; + +import java.util.Set; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.apache.airavata.research.service.model.UserContext; +import org.apache.airavata.research.service.model.entity.Resource; +import org.apache.airavata.research.service.model.entity.ResourceVerificationActivity; +import org.apache.airavata.research.service.model.repo.ResourceRepository; +import org.apache.airavata.research.service.model.repo.ResourceVerificationActivityRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class AdminHandler { + + private ResourceRepository resourceRepository; + private ResourceVerificationActivityRepository verificationActivityRepository; + + @Value("#{'${airavata.research-portal.admin-emails}'.split(',')}") + private Set cybershuttleAdminEmails; + + public AdminHandler( + ResourceRepository resourceRepository, + ResourceVerificationActivityRepository verificationActivityRepository) { + this.resourceRepository = resourceRepository; + this.verificationActivityRepository = verificationActivityRepository; + } + + public Resource verifyResource(Resource resource) { + if (resource.getStatus().equals(StatusEnum.VERIFIED)) { + return resource; + } + String userId = UserContext.userId(); + ensureAdminPermissions(userId); + + resource.setStatus(StatusEnum.VERIFIED); + resourceRepository.save(resource); + + ResourceVerificationActivity activity = new ResourceVerificationActivity(); + activity.setResource(resource); + activity.setUserId(userId); + activity.setStatus(StatusEnum.VERIFIED); + verificationActivityRepository.save(activity); + + return resource; + } + + public Resource rejectResource(Resource resource, String rejectionMessage) { + String userId = UserContext.userId(); + ensureAdminPermissions(userId); + String cleanMessage = rejectionMessage.trim().replaceAll("\"", ""); + + resource.setStatus(StatusEnum.REJECTED); + resourceRepository.save(resource); + + ResourceVerificationActivity activity = new ResourceVerificationActivity(); + activity.setResource(resource); + activity.setUserId(userId); + activity.setMessage(cleanMessage); + activity.setStatus(StatusEnum.REJECTED); + verificationActivityRepository.save(activity); + + return resource; + } + + private void ensureAdminPermissions(String userId) { + if (!cybershuttleAdminEmails.contains(userId)) { + throw new RuntimeException(String.format("User %s does not have admin access in Cybershuttle", userId)); + } + } +} diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java index d76b11aae6..b43936e819 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java @@ -37,14 +37,18 @@ import org.apache.airavata.research.service.model.UserContext; import org.apache.airavata.research.service.model.entity.RepositoryResource; import org.apache.airavata.research.service.model.entity.Resource; +import org.apache.airavata.research.service.model.entity.ResourceAuthor; import org.apache.airavata.research.service.model.entity.ResourceStar; +import org.apache.airavata.research.service.model.entity.ResourceVerificationActivity; import org.apache.airavata.research.service.model.entity.Tag; import org.apache.airavata.research.service.model.repo.ProjectRepository; import org.apache.airavata.research.service.model.repo.ResourceRepository; import org.apache.airavata.research.service.model.repo.ResourceStarRepository; +import org.apache.airavata.research.service.model.repo.ResourceVerificationActivityRepository; import org.apache.airavata.research.service.model.repo.TagRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -61,29 +65,39 @@ public class ResourceHandler { private final ResourceRepository resourceRepository; private final ProjectRepository projectRepository; private final ResourceStarRepository resourceStarRepository; + private final ResourceVerificationActivityRepository verificationActivityRepository; + + @Value("#{'${airavata.research-portal.admin-emails}'.split(',')}") + private Set cybershuttleAdminEmails; public ResourceHandler( AiravataService airavataService, TagRepository tagRepository, ResourceRepository resourceRepository, ProjectRepository projectRepository, - ResourceStarRepository resourceStarRepository) { + ResourceStarRepository resourceStarRepository, + ResourceVerificationActivityRepository verificationActivityRepository) { this.airavataService = airavataService; this.tagRepository = tagRepository; this.resourceRepository = resourceRepository; this.projectRepository = projectRepository; this.resourceStarRepository = resourceStarRepository; + this.verificationActivityRepository = verificationActivityRepository; } public void initializeResource(Resource resource) { - Set userSet = new HashSet<>(); - for (String authorId : resource.getAuthors()) { + Set userSet = new HashSet<>(); + for (ResourceAuthor author : resource.getAuthors()) { try { - UserProfile fetchedUser = airavataService.getUserProfile(authorId); - userSet.add(fetchedUser.getUserId()); + UserProfile fetchedUser = airavataService.getUserProfile(author.getAuthorId()); + ResourceAuthor newAuthor = new ResourceAuthor(); + newAuthor.setAuthorId(fetchedUser.getUserId()); + newAuthor.setRole(author.getRole()); + userSet.add(newAuthor); } catch (Exception e) { - LOGGER.error("Error while fetching user profile with the userId: {}", authorId, e); - throw new EntityNotFoundException("Error while fetching user profile with the userId: " + authorId, e); + LOGGER.error("Error while fetching user profile with the userId: {}", author.getAuthorId(), e); + throw new EntityNotFoundException( + "Error while fetching user profile with the userId: " + author.getAuthorId(), e); } } @@ -115,10 +129,10 @@ public void transferResourceRequestFields(Resource resource, CreateResourceReque // check that the logged in author is at least one of the authors making the request String currentUserId = UserContext.userId(); boolean found = false; - for (String authorId : createResourceRequest.getAuthors()) { - if (authorId.equalsIgnoreCase(currentUserId)) { + for (ResourceAuthor author : createResourceRequest.getAuthors()) { + author.setAuthorId(author.getAuthorId().toLowerCase()); + if (author.getAuthorId().equalsIgnoreCase(currentUserId)) { found = true; - break; } } if (!found) { @@ -128,9 +142,7 @@ public void transferResourceRequestFields(Resource resource, CreateResourceReque resource.setName(createResourceRequest.getName()); resource.setDescription(createResourceRequest.getDescription()); - resource.setAuthors(createResourceRequest.getAuthors().stream() - .map(String::toLowerCase) - .collect(Collectors.toSet())); + resource.setAuthors(createResourceRequest.getAuthors()); Set tagsSet = new HashSet<>(); for (String tag : createResourceRequest.getTags()) { org.apache.airavata.research.service.model.entity.Tag t = @@ -164,8 +176,8 @@ public Resource modifyResource(ModifyResourceRequest resourceRequest) { // ensure that the user making the request is one of the current authors boolean found = false; - for (String authorId : resource.getAuthors()) { - if (authorId.equalsIgnoreCase(UserContext.userId())) { + for (ResourceAuthor author : resource.getAuthors()) { + if (author.getAuthorId().equalsIgnoreCase(UserContext.userId())) { found = true; break; } @@ -238,7 +250,10 @@ public Resource getResourceById(String id) { if (resource.getPrivacy().equals(PrivacyEnum.PUBLIC)) { return resource; } else if (isAuthenticated - && resource.getAuthors().contains(UserContext.userId().toLowerCase())) { + && resource.getAuthors().stream() + .map(ResourceAuthor::getAuthorId) + .anyMatch( + authorId -> authorId.equals(UserContext.userId().toLowerCase()))) { return resource; } else { throw new EntityNotFoundException("Resource not found: " + id); @@ -255,7 +270,9 @@ public boolean deleteResourceById(String id) { Resource resource = opResource.get(); String userEmail = UserContext.userId(); - if (!resource.getAuthors().contains(userEmail.toLowerCase())) { + if (!resource.getAuthors().stream() + .map(ResourceAuthor::getAuthorId) + .anyMatch(authorId -> authorId.equals(UserContext.userId().toLowerCase()))) { String errorMsg = String.format( "User %s not authorized to delete resource: %s (%s), type: %s", userEmail, resource.getName(), id, resource.getType().toString()); @@ -293,6 +310,43 @@ public List getAllResourcesByTypeAndName(Class typ return resourceRepository.findByTypeAndNameContainingIgnoreCase(type, name.toLowerCase(), UserContext.userId()); } + public Resource submitResourceForVerification(String id) { + Resource resource = getResourceById(id); + String userId = UserContext.userId(); + + if (!isResourceAuthor(resource, userId)) { + throw new IllegalArgumentException( + String.format("User %s is not authorized to request verification for resource %s", userId, id)); + } + + resource.setStatus(StatusEnum.PENDING); + resourceRepository.save(resource); + + ResourceVerificationActivity activity = new ResourceVerificationActivity(); + activity.setResource(resource); + activity.setUserId(userId); + activity.setStatus(StatusEnum.PENDING); + verificationActivityRepository.save(activity); + + return resource; + } + + public List getResourceVerificationActivities(String id) { + Resource resource = getResourceById(id); + String userId = UserContext.userId(); + + if (!isResourceAuthor(resource, userId) && !cybershuttleAdminEmails.contains(userId.toLowerCase())) { + throw new IllegalArgumentException(String.format( + "User %s is not authorized to pull verification activities for resource %s", userId, id)); + } + + return verificationActivityRepository.findAllByResourceOrderByUpdatedAtDesc(resource); + } + + public List getAllResourcesWithStatus(List includeStatus) { + return resourceRepository.findAllByStatusInOrderByCreatedAtDesc(includeStatus); + } + private Page getAllPublicResources( int pageNumber, int pageSize, List> typeList, String[] tag, String nameSearch) { Pageable pageable = PageRequest.of(pageNumber, pageSize); @@ -320,4 +374,8 @@ private Page getAllResourcesUserSignedIn( return resourceRepository.findAllByTypesAndAllTagsForUser( typeList, tag, (long) tag.length, nameSearch.toLowerCase(), userId, pageable); } + + private boolean isResourceAuthor(Resource resource, String userId) { + return resource.getAuthors().stream().map(ResourceAuthor::getAuthorId).anyMatch(userId::equals); + } } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java index 97814f3163..b78cf72942 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java @@ -19,10 +19,9 @@ */ package org.apache.airavata.research.service.model.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.CascadeType; -import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; @@ -35,6 +34,7 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.Instant; import java.util.HashSet; @@ -69,10 +69,9 @@ public abstract class Resource { @Column(nullable = false) private String headerImage; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "resource_authors", joinColumns = @JoinColumn(name = "resource_id")) - @Column(name = "author_id") - private Set authors = new HashSet<>(); + @OneToMany(mappedBy = "resource", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) + @JsonManagedReference + private Set authors = new HashSet<>(); @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER) @JoinTable( @@ -135,11 +134,12 @@ public void setDescription(String description) { this.description = description; } - public Set getAuthors() { + public Set getAuthors() { return authors; } - public void setAuthors(Set authors) { + public void setAuthors(Set authors) { + authors.forEach(author -> author.setResource(this)); this.authors = authors; } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceAuthor.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceAuthor.java new file mode 100644 index 0000000000..71ff006c2c --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceAuthor.java @@ -0,0 +1,79 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.model.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.apache.airavata.research.service.enums.AuthorRoleEnum; +import org.hibernate.annotations.UuidGenerator; + +@Entity +@Table(name = "resource_authors") +public class ResourceAuthor { + @Id + @GeneratedValue + @UuidGenerator + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "resource_id", nullable = false) + @JsonBackReference + private Resource resource; + + @Column(name = "author_id") + private String authorId; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private AuthorRoleEnum role; + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public String getAuthorId() { + return authorId; + } + + public void setAuthorId(String authorId) { + this.authorId = authorId; + } + + public AuthorRoleEnum getRole() { + return role; + } + + public void setRole(AuthorRoleEnum role) { + this.role = role; + } +} diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceVerificationActivity.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceVerificationActivity.java new file mode 100644 index 0000000000..355bb66321 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceVerificationActivity.java @@ -0,0 +1,115 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.model.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.Instant; +import org.apache.airavata.research.service.enums.StatusEnum; +import org.hibernate.annotations.UuidGenerator; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity(name = "RESOURCE_VERIFICATION_ACTIVITY") +@EntityListeners(AuditingEntityListener.class) +public class ResourceVerificationActivity { + + @Id + @GeneratedValue + @UuidGenerator + @Column(nullable = false, updatable = false, length = 48) + private String id; + + @ManyToOne(optional = false) + @JoinColumn(name = "resource_id") + @JsonBackReference + private Resource resource; + + @Column(name = "user_id", nullable = false) + private String userId; // can't use ResourceAuthor because admins are not authors, and they can also make activities + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private StatusEnum status; + + @Column + private String message; + + @Column(nullable = false, updatable = false) + @CreatedDate + private Instant createdAt; + + @Column(nullable = false) + @LastModifiedDate + private Instant updatedAt; + + public String getId() { + return id; + } + + public Resource getResource() { + return resource; + } + + public void setResource(Resource resource) { + this.resource = resource; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public StatusEnum getStatus() { + return status; + } + + public void setStatus(StatusEnum status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } +} diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java index 98e7b27a2f..30c998ac70 100644 --- a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java @@ -19,9 +19,11 @@ */ package org.apache.airavata.research.service.model.repo; +import java.util.Collection; import java.util.List; import java.util.Optional; import org.apache.airavata.research.service.enums.StateEnum; +import org.apache.airavata.research.service.enums.StatusEnum; import org.apache.airavata.research.service.model.entity.Resource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -50,15 +52,15 @@ Page findAllByTypes( @Query( """ - SELECT DISTINCT r - FROM Resource r - JOIN r.authors a - WHERE r.class IN :typeList - AND LOWER(r.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) - AND r.state = 'ACTIVE' - AND (r.privacy = 'PUBLIC' or a = :userId) - ORDER BY r.name - """) + SELECT DISTINCT r + FROM Resource r + JOIN r.authors a + WHERE r.class IN :typeList + AND LOWER(r.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) + AND r.state = 'ACTIVE' + AND (r.privacy = 'PUBLIC' or a.authorId = :userId) + ORDER BY r.name + """) Page findAllByTypesForUser( @Param("typeList") List> typeList, @Param("nameSearch") String nameSearch, @@ -88,19 +90,19 @@ Page findAllByTypesAndAllTags( @Query( """ - SELECT r - FROM Resource r - JOIN r.tags t - JOIN r.authors a - WHERE r.class IN :typeList - AND t.value IN :tags - AND LOWER(r.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) - AND r.state = 'ACTIVE' - AND (r.privacy = 'PUBLIC' OR a = :userId) - GROUP BY r - HAVING COUNT(DISTINCT t.value) = :tagCount - ORDER BY r.name - """) + SELECT r + FROM Resource r + JOIN r.tags t + JOIN r.authors a + WHERE r.class IN :typeList + AND t.value IN :tags + AND LOWER(r.name) LIKE LOWER(CONCAT('%', :nameSearch, '%')) + AND r.state = 'ACTIVE' + AND (r.privacy = 'PUBLIC' OR a.authorId = :userId) + GROUP BY r + HAVING COUNT(DISTINCT t.value) = :tagCount + ORDER BY r.name + """) Page findAllByTypesAndAllTagsForUser( @Param("typeList") List> typeList, @Param("tags") String[] tags, @@ -116,10 +118,12 @@ Page findAllByTypesAndAllTagsForUser( JOIN r.authors a WHERE TYPE(r) = :type AND r.state = 'ACTIVE' AND LOWER(r.name) LIKE LOWER(CONCAT('%', :name, '%')) - AND (r.privacy = "PUBLIC" OR a = :userId) + AND (r.privacy = "PUBLIC" OR a.authorId = :userId) """) List findByTypeAndNameContainingIgnoreCase( @Param("type") Class type, @Param("name") String name, @Param("userId") String userId); Optional findByIdAndState(String id, StateEnum state); + + List findAllByStatusInOrderByCreatedAtDesc(Collection statuses); } diff --git a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceVerificationActivityRepository.java b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceVerificationActivityRepository.java new file mode 100644 index 0000000000..a9ca261844 --- /dev/null +++ b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceVerificationActivityRepository.java @@ -0,0 +1,33 @@ +/** +* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.apache.airavata.research.service.model.repo; + +import java.util.List; +import org.apache.airavata.research.service.model.entity.Resource; +import org.apache.airavata.research.service.model.entity.ResourceVerificationActivity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ResourceVerificationActivityRepository + extends JpaRepository, + JpaSpecificationExecutor { + + List findAllByResourceOrderByUpdatedAtDesc(Resource resource); +} diff --git a/modules/research-framework/research-service/src/main/resources/application.yml b/modules/research-framework/research-service/src/main/resources/application.yml index 0652644a64..8f1bde858b 100644 --- a/modules/research-framework/research-service/src/main/resources/application.yml +++ b/modules/research-framework/research-service/src/main/resources/application.yml @@ -29,10 +29,12 @@ airavata: adminApiKey: "JUPYTER_ADMIN_API_KEY" limit: 10 research-portal: - url: http://airavata.host:5173 - dev-url: http://airavata.host:5173 + url: http://localhost:5173 + dev-url: http://localhost:5173 + admin-notification-email: "TO_EMAIL@gmail.com" + admin-emails: ganning.xu@gatech.edu,ljayathilake3@gatech.edu,yasith@gatech.edu,smarru@gatech.edu,dimuthuw@gatech.edu openid: - url: "http://airavata.host:18080/realms/default" + url: "http://localhost:18080/realms/default" user-profile: server: url: airavata.host @@ -55,6 +57,17 @@ spring: hibernate: ddl-auto: update open-in-view: false + mail: + host: smtp.gmail.com + port: 587 + username: youremail@gmail.com + password: your-app-password # Use an App Password from Google + properties: + mail: + smtp: + auth: true + starttls: + enable: true springdoc: api-docs: diff --git a/pom.xml b/pom.xml index edafd19681..a5d5ea9851 100644 --- a/pom.xml +++ b/pom.xml @@ -353,9 +353,9 @@ under the License. 2.1.3 - jakarta.transaction - jakarta.transaction-api - 2.0.1 + jakarta.transaction + jakarta.transaction-api + 2.0.1 javax.xml.bind @@ -612,7 +612,7 @@ under the License. ${skipTests} ${project.build.testOutputDirectory} false - -Xmx1024m -XX:MaxPermSize=256m --add-opens java.base/java.lang=ALL-UNNAMED + -Xmx1024m --add-opens java.base/java.lang=ALL-UNNAMED -javaagent:${settings.localRepository}/org/jmockit/jmockit/1.50/jmockit-1.50.jar false