Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
Expand Down Expand Up @@ -39,4 +40,4 @@ bin/
.vscode/

### Mac OS ###
.DS_Store
.DS_Store
10 changes: 9 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id("java")
id("io.freefair.lombok") version "9.1.0"
}

group = "org.lab"
Expand All @@ -9,12 +10,19 @@ repositories {
mavenCentral()
}

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}

dependencies {
implementation("org.jetbrains:annotations:26.0.2")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
useJUnitPlatform()
}
}
Empty file modified gradlew
100644 → 100755
Empty file.
22 changes: 22 additions & 0 deletions src/main/java/org/lab/auth/AuthRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.lab.auth;

import org.lab.auth.model.AccessBinding;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface AuthRepository {
AccessBinding save(AccessBinding accessBinding);

Optional<AccessBinding> findByUserIdAndProjectId(UUID userId, UUID projectId);

List<AccessBinding> findAll();

void deleteByUserIdAndProjectId(UUID userId, UUID projectId);

default void delete(AccessBinding binding) {
deleteByUserIdAndProjectId(binding.userId(), binding.projectId());
}
}

21 changes: 21 additions & 0 deletions src/main/java/org/lab/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.lab.auth;

import org.lab.auth.model.AccessBinding;
import org.lab.auth.model.Permission;
import org.lab.auth.model.Role;

import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

public interface AuthService {
void checkPermission(UUID projectId, Permission permission);

void addBinding(UUID userId, UUID projectId, Role role);

void removeBinding(UUID userId, UUID projectId, Role role);

List<AccessBinding> findAllByUserId(UUID userId);

void removeAllByProjectIdAndRole(UUID projectId, Role role);
}
62 changes: 62 additions & 0 deletions src/main/java/org/lab/auth/AuthServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.lab.auth;

import org.lab.auth.model.AccessBinding;
import org.lab.auth.model.Permission;
import org.lab.auth.model.Role;

import java.util.List;
import java.util.UUID;
import java.util.function.Supplier;

public class AuthServiceImpl implements AuthService {
private final AuthRepository authRepository;

public AuthServiceImpl(AuthRepository authRepository) {
this.authRepository = authRepository;
}

@Override
public void checkPermission(UUID projectId, Permission permission) {
var userId = AuthenticationContext.get();
if (!hasPermission(userId, projectId, permission)) {
throw new PermissionDeniedException(userId, projectId, permission);
}
}

private boolean hasPermission(UUID userId, UUID projectId, Permission permission) {
var binding = authRepository.findByUserIdAndProjectId(userId, projectId)
.orElse(null);

return binding != null && binding.role().getPermissions().contains(permission.getName());
}

@Override
public void addBinding(UUID userId, UUID projectId, Role role) {
authRepository.save(new AccessBinding(userId, projectId, role));
}

@Override
public void removeBinding(UUID userId, UUID projectId, Role role) {
var binding = authRepository.findByUserIdAndProjectId(userId, projectId)
.orElse(null);

if (binding != null && binding.role() == role) {
authRepository.deleteByUserIdAndProjectId(userId, projectId);
}
}

@Override
public List<AccessBinding> findAllByUserId(UUID userId) {
return authRepository.findAll().stream()
.filter(binding -> binding.userId().equals(userId))
.toList();
}

@Override
public void removeAllByProjectIdAndRole(UUID projectId, Role role) {
authRepository.findAll().stream()
.filter(binding -> binding.projectId().equals(projectId) && binding.role() == role)
.forEach(authRepository::delete);
}
}

21 changes: 21 additions & 0 deletions src/main/java/org/lab/auth/AuthenticationContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.lab.auth;

import java.util.UUID;

public class AuthenticationContext {

private static final ThreadLocal<UUID> USER_ID = new ThreadLocal<>();

public static UUID get() {
return USER_ID.get();
}

public static void set(UUID userId) {
USER_ID.set(userId);
}

public static void clear() {
USER_ID.remove();
}

}
40 changes: 40 additions & 0 deletions src/main/java/org/lab/auth/InMemoryAuthRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.lab.auth;

import org.lab.auth.model.AccessBinding;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class InMemoryAuthRepository implements AuthRepository {
private final Map<String, AccessBinding> storage = new ConcurrentHashMap<>();

private String key(UUID userId, UUID projectId) {
return userId + ":" + projectId;
}

@Override
public AccessBinding save(AccessBinding accessBinding) {
storage.put(key(accessBinding.userId(), accessBinding.projectId()), accessBinding);
return accessBinding;
}

@Override
public Optional<AccessBinding> findByUserIdAndProjectId(UUID userId, UUID projectId) {
return Optional.ofNullable(storage.get(key(userId, projectId)));
}

@Override
public List<AccessBinding> findAll() {
return new ArrayList<>(storage.values());
}

@Override
public void deleteByUserIdAndProjectId(UUID userId, UUID projectId) {
storage.remove(key(userId, projectId));
}
}

12 changes: 12 additions & 0 deletions src/main/java/org/lab/auth/PermissionDeniedException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.lab.auth;

import org.lab.auth.model.Permission;

import java.util.UUID;

public class PermissionDeniedException extends RuntimeException {
public PermissionDeniedException(UUID userId, UUID projectId, Permission permission) {
super("Permission denied: userId=" + userId + ", projectId=" + projectId + ", permission=" + permission.getName());
}
}

11 changes: 11 additions & 0 deletions src/main/java/org/lab/auth/model/AccessBinding.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.lab.auth.model;

import java.util.UUID;

public record AccessBinding(
UUID userId,
UUID projectId,
Role role
) {
}

38 changes: 38 additions & 0 deletions src/main/java/org/lab/auth/model/Permission.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.lab.auth.model;

public enum Permission {
PROJECT_SET_TEAM_LEAD("project.setTeamLead", "Set team lead for a project"),
PROJECT_ADD_DEVELOPER("project.addDeveloper", "Add developer to a project"),
PROJECT_ADD_TESTER("project.addTester", "Add tester to a project"),
PROJECT_TEST("project.test", "Test a project"),

TICKET_CREATE("ticket.create", "Create a new ticket"),
TICKET_ASSIGN_DEVELOPER("ticket.assignDeveloper", "Assign developer to a ticket"),
TICKET_GET_STATUS("ticket.getStatus", "Get ticket status"),
TICKET_COMPLETE("ticket.complete", "Complete a ticket"),

BUG_REPORT_CREATE("bugReport.create", "Create a new bug report"),
BUG_REPORT_FIX("bugReport.fix", "Mark bug report as fixed"),
BUG_REPORT_TEST("bugReport.test", "Mark bug report as tested"),
BUG_REPORT_CLOSE("bugReport.close", "Close a bug report"),

MILESTONE_CREATE("milestone.create", "Create a new milestone"),
MILESTONE_SET_STATUS("milestone.setStatus", "Set milestone status");

private final String name;
private final String description;

Permission(String name, String description) {
this.name = name;
this.description = description;
}

public String getName() {
return name;
}

public String getDescription() {
return description;
}
}

54 changes: 54 additions & 0 deletions src/main/java/org/lab/auth/model/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.lab.auth.model;

import java.util.List;

public enum Role {
MANAGER("manager", List.of(
Permission.PROJECT_SET_TEAM_LEAD.getName(),
Permission.PROJECT_ADD_DEVELOPER.getName(),
Permission.PROJECT_ADD_TESTER.getName(),
Permission.TICKET_CREATE.getName(),
Permission.TICKET_ASSIGN_DEVELOPER.getName(),
Permission.TICKET_GET_STATUS.getName(),
Permission.MILESTONE_CREATE.getName(),
Permission.MILESTONE_SET_STATUS.getName()
)),

DEVELOPER("developer", List.of(
Permission.TICKET_COMPLETE.getName(),
Permission.BUG_REPORT_CREATE.getName(),
Permission.BUG_REPORT_FIX.getName(),
Permission.BUG_REPORT_CLOSE.getName()
)),

TESTER("tester", List.of(
Permission.PROJECT_TEST.getName(),
Permission.BUG_REPORT_CREATE.getName(),
Permission.BUG_REPORT_TEST.getName(),
Permission.BUG_REPORT_CLOSE.getName()
)),

TEAM_LEAD("teamLead", List.of(
Permission.TICKET_CREATE.getName(),
Permission.TICKET_ASSIGN_DEVELOPER.getName(),
Permission.TICKET_GET_STATUS.getName(),
Permission.TICKET_COMPLETE.getName()
));

private final String name;
private final List<String> permissions;

Role(String name, List<String> permissions) {
this.name = name;
this.permissions = permissions;
}

public String getName() {
return name;
}

public List<String> getPermissions() {
return permissions;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.lab.exception;

import java.util.UUID;

public class ActiveMilestoneExistsException extends RuntimeException {
public ActiveMilestoneExistsException(UUID projectId) {
super("Project already has an active milestone: " + projectId);
}
}

15 changes: 15 additions & 0 deletions src/main/java/org/lab/exception/BugReportNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.lab.exception;

import java.util.UUID;
import java.util.function.Supplier;

public class BugReportNotFoundException extends RuntimeException {
public BugReportNotFoundException(UUID bugReportId) {
super("Bug report not found: " + bugReportId);
}

public static Supplier<BugReportNotFoundException> supplier(UUID bugReportId) {
return () -> new BugReportNotFoundException(bugReportId);
}
}

15 changes: 15 additions & 0 deletions src/main/java/org/lab/exception/MilestoneNotFoundException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.lab.exception;

import java.util.UUID;
import java.util.function.Supplier;

public class MilestoneNotFoundException extends RuntimeException {
public MilestoneNotFoundException(UUID milestoneId) {
super("Milestone not found: " + milestoneId);
}

public static Supplier<MilestoneNotFoundException> supplier(UUID milestoneId) {
return () -> new MilestoneNotFoundException(milestoneId);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.lab.exception;

import java.util.UUID;

public class NotAllTicketsCompletedException extends RuntimeException {
public NotAllTicketsCompletedException(UUID milestoneId) {
super("Cannot close milestone: not all tickets are completed: " + milestoneId);
}
}

Loading