Skip to content

Commit

Permalink
[kbss-cvut/termit-ui#571] Authorize TermOccurrence modifications (app…
Browse files Browse the repository at this point in the history
…roval, removal, etc.).
  • Loading branch information
ledsoft committed Nov 25, 2024
1 parent d487461 commit 5f90283
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 13 deletions.
21 changes: 15 additions & 6 deletions src/main/java/cz/cvut/kbss/termit/persistence/dao/TermDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,30 @@ protected URI labelProperty() {
@Override
public Optional<Term> find(URI id) {
try {
final Optional<Term> result = resolveVocabularyId(id).map(vocabulary ->
em.find(Term.class, id, descriptorFactory.termDescriptor(vocabulary)));
final Optional<Term> result = findTermVocabulary(id).map(vocabulary ->
em.find(Term.class, id,
descriptorFactory.termDescriptor(
vocabulary)));
result.ifPresent(this::postLoad);
return result;
} catch (RuntimeException e) {
throw new PersistenceException(e);
}
}

private Optional<URI> resolveVocabularyId(URI termId) {
/**
* Finds vocabulary to which a term with the specified id belongs.
*
* @param termId Term identifier
* @return Vocabulary identifier wrapped in {@code Optional}
*/
public Optional<URI> findTermVocabulary(URI termId) {
Objects.requireNonNull(termId);
try {
return Optional.of(em.createNativeQuery("SELECT DISTINCT ?v WHERE { ?t ?inVocabulary ?v . }", URI.class)
.setParameter("t", termId)
.setParameter("inVocabulary", TERM_FROM_VOCABULARY)
.getSingleResult());
.setParameter("t", termId)
.setParameter("inVocabulary", TERM_FROM_VOCABULARY)
.getSingleResult());
} catch (NoResultException | NoUniqueResultException e) {
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ public TermOccurrenceController(IdentifierResolver idResolver, Configuration con
})
@PutMapping(consumes = {JsonLd.MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_FULL_USER + "')")
public void saveOccurrence(@Parameter(description = "Term occurrence to save")
@RequestBody TermOccurrence occurrence) {
occurrenceService.persistOrUpdate(occurrence);
Expand All @@ -91,7 +90,6 @@ public void saveOccurrence(@Parameter(description = "Term occurrence to save")
})
@PutMapping(value = "/{localName}")
@ResponseStatus(HttpStatus.ACCEPTED)
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_FULL_USER + "')")
public void approveOccurrence(
@Parameter(description = TermOccurrenceControllerDoc.ID_LOCAL_NAME_DESCRIPTION,
example = TermOccurrenceControllerDoc.ID_LOCAL_NAME_EXAMPLE)
Expand All @@ -113,7 +111,6 @@ public void approveOccurrence(
})
@DeleteMapping(value = "/{localName}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_FULL_USER + "')")
public void removeOccurrence(@Parameter(description = TermOccurrenceControllerDoc.ID_LOCAL_NAME_DESCRIPTION,
example = TermOccurrenceControllerDoc.ID_LOCAL_NAME_EXAMPLE)
@PathVariable String localName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -58,6 +59,7 @@ public TermOccurrenceRepositoryService(TermOccurrenceDao termOccurrenceDao, Term
this.resourceService = resourceService;
}

@PreAuthorize("@termOccurrenceAuthorizationService.canModify(#occurrence)")
@Transactional
@Override
public void persist(TermOccurrence occurrence) {
Expand All @@ -78,6 +80,7 @@ private void checkTermExists(TermOccurrence occurrence) {
}
}

@PreAuthorize("@termOccurrenceAuthorizationService.canModify(#occurrence)")
@Transactional
@Override
public void persistOrUpdate(TermOccurrence occurrence) {
Expand All @@ -95,6 +98,7 @@ public void persistOrUpdate(TermOccurrence occurrence) {
}
}

@PreAuthorize("@termOccurrenceAuthorizationService.canModify(#occurrenceId)")
@Async
// Retry in case the occurrence has not been persisted, yet (see AsynchronousTermOccurrenceSaver)
@Retryable(retryFor = NotFoundException.class, maxAttempts = 3, backoff = @Backoff(delay = 30000L))
Expand All @@ -108,6 +112,7 @@ public void approve(URI occurrenceId) {
toApprove.markApproved();
}

@PreAuthorize("@termOccurrenceAuthorizationService.canModify(#occurrenceId)")
@Transactional
@Override
public void remove(URI occurrenceId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,17 @@ public List<TermOccurrence> getDefinitionallyRelatedOf(Term instance) {
return termOccurrenceDao.findAllDefinitionalOf(instance);
}

/**
* Gets the identifier of a vocabulary to which a term with the specified id belongs.
*
* @param termId Term identifier
* @return Vocabulary identifier wrapped in {@code Optional}
*/
@Transactional(readOnly = true)
public Optional<URI> findTermVocabulary(URI termId) {
return termDao.findTermVocabulary(termId);
}

/**
* Checks that a term can be removed.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ public boolean canModify(Resource asset) {
}

private Optional<Vocabulary> resolveVocabulary(Resource resource) {
if (resource instanceof Document) {
final URI vocIri = ((Document) resource).getVocabulary();
if (resource instanceof Document document) {
final URI vocIri = document.getVocabulary();
return vocIri != null ? Optional.of(new Vocabulary(vocIri)) : Optional.empty();
} else if (resource instanceof File) {
final File f = (File) resource;
} else if (resource instanceof File f) {
return f.getDocument() != null ? getDocumentVocabulary(f.getDocument()) : Optional.empty();
}
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cz.cvut.kbss.termit.service.security.authorization;

import cz.cvut.kbss.termit.model.Vocabulary;
import cz.cvut.kbss.termit.model.assignment.TermDefinitionalOccurrence;
import cz.cvut.kbss.termit.model.assignment.TermFileOccurrence;
import cz.cvut.kbss.termit.model.assignment.TermOccurrence;
import cz.cvut.kbss.termit.model.resource.Resource;
import cz.cvut.kbss.termit.persistence.dao.TermOccurrenceDao;
import cz.cvut.kbss.termit.service.repository.ResourceRepositoryService;
import cz.cvut.kbss.termit.service.repository.TermRepositoryService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.URI;
import java.util.Objects;
import java.util.Optional;

@Service
public class TermOccurrenceAuthorizationService {

private final TermOccurrenceDao dao;

private final TermRepositoryService termService;

private final ResourceRepositoryService resourceService;

private final VocabularyAuthorizationService vocabularyAuthorizationService;

private final ResourceAuthorizationService resourceAuthorizationService;

public TermOccurrenceAuthorizationService(TermOccurrenceDao dao, TermRepositoryService termService,
ResourceRepositoryService resourceService,
VocabularyAuthorizationService vocabularyAuthorizationService,
ResourceAuthorizationService resourceAuthorizationService) {
this.dao = dao;
this.termService = termService;
this.resourceService = resourceService;
this.vocabularyAuthorizationService = vocabularyAuthorizationService;
this.resourceAuthorizationService = resourceAuthorizationService;
}

@Transactional(readOnly = true)
public boolean canModify(TermOccurrence occurrence) {
Objects.requireNonNull(occurrence);
if (occurrence instanceof TermDefinitionalOccurrence definitionalOccurrence) {
final Optional<URI> vocabularyUri = termService.findTermVocabulary(
definitionalOccurrence.getTarget().getSource());
return vocabularyUri.map(vUri -> vocabularyAuthorizationService.canModify(new Vocabulary(vUri)))
.orElse(false);
} else {
final TermFileOccurrence fo = (TermFileOccurrence) occurrence;
final Optional<Resource> file = resourceService.find(fo.getTarget().getSource());
return file.map(resourceAuthorizationService::canModify).orElse(false);
}
}

@Transactional(readOnly = true)
public boolean canModify(URI occurrenceId) {
return dao.find(occurrenceId).map(this::canModify).orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package cz.cvut.kbss.termit.service.security.authorization;

import cz.cvut.kbss.termit.environment.Generator;
import cz.cvut.kbss.termit.model.Vocabulary;
import cz.cvut.kbss.termit.model.assignment.DefinitionalOccurrenceTarget;
import cz.cvut.kbss.termit.model.assignment.FileOccurrenceTarget;
import cz.cvut.kbss.termit.model.assignment.TermDefinitionalOccurrence;
import cz.cvut.kbss.termit.model.assignment.TermFileOccurrence;
import cz.cvut.kbss.termit.model.assignment.TermOccurrence;
import cz.cvut.kbss.termit.model.resource.File;
import cz.cvut.kbss.termit.persistence.dao.TermOccurrenceDao;
import cz.cvut.kbss.termit.service.repository.ResourceRepositoryService;
import cz.cvut.kbss.termit.service.repository.TermRepositoryService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.net.URI;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class TermOccurrenceAuthorizationServiceTest {

@Mock
private TermOccurrenceDao toDao;

@Mock
private TermRepositoryService termService;

@Mock
private ResourceRepositoryService resourceService;

@Mock
private VocabularyAuthorizationService vocabularyAuthorizationService;

@Mock
private ResourceAuthorizationService resourceAuthorizationService;

@InjectMocks
private TermOccurrenceAuthorizationService sut;

@Test
void canModifyResolvesTermVocabularyAndChecksIfUserCanModifyItWhenTermOccurrenceIsDefinitional() {
final URI vocabularyUri = Generator.generateUri();
final TermOccurrence to = new TermDefinitionalOccurrence(Generator.generateUri(),
new DefinitionalOccurrenceTarget(
Generator.generateTermWithId(vocabularyUri)));
to.setUri(Generator.generateUri());
when(termService.findTermVocabulary(to.getTarget().getSource())).thenReturn(Optional.of(vocabularyUri));
when(vocabularyAuthorizationService.canModify(new Vocabulary(vocabularyUri))).thenReturn(true);
when(toDao.find(to.getUri())).thenReturn(Optional.of(to));

assertTrue(sut.canModify(to.getUri()));
verify(vocabularyAuthorizationService).canModify(new Vocabulary(vocabularyUri));
}

@Test
void canModifyResolvesResourceVocabularyAndChecksIfUserCanModifyItWhenTermOccurrenceIsFileOccurrence() {
final URI vocabularyUri = Generator.generateUri();
final File file = Generator.generateFileWithId("test.html");
file.setDocument(Generator.generateDocumentWithId());
file.getDocument().setVocabulary(vocabularyUri);
final TermOccurrence to = new TermFileOccurrence(Generator.generateUri(), new FileOccurrenceTarget(file));
to.setUri(Generator.generateUri());
when(resourceService.find(file.getUri())).thenReturn(Optional.of(file));
when(resourceAuthorizationService.canModify(file)).thenReturn(true);
when(toDao.find(to.getUri())).thenReturn(Optional.of(to));

assertTrue(sut.canModify(to.getUri()));
verify(resourceAuthorizationService).canModify(file);
}

@Test
void canModifyReturnsFalseWhenTermOccurrenceDoesNotExist() {
when(toDao.find(any())).thenReturn(Optional.empty());
assertFalse(sut.canModify(Generator.generateUri()));
}
}

0 comments on commit 5f90283

Please sign in to comment.