Skip to content
Merged
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
953 changes: 481 additions & 472 deletions contract/openapi.yaml

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/main/java/kr/ac/pusan/pickle/access/ResourceAccessGrant.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.UpdateTimestamp;
Expand All @@ -33,6 +34,14 @@ public class ResourceAccessGrant {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* The identifier this row wears outside the API boundary. Internal joins,
* sorts and foreign keys keep using {@link #id}.
*/
@JdbcTypeCode(SqlTypes.UUID)
@Column(name = "public_id", nullable = false, updatable = false, unique = true)
private UUID publicId = UUID.randomUUID();

@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
@Column(name = "resource_type", nullable = false, columnDefinition = "resource_type")
Expand Down Expand Up @@ -90,6 +99,10 @@ public Long getId() {
return id;
}

public UUID getPublicId() {
return publicId;
}

public ResourceType getResourceType() {
return resourceType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ResourceAccessGrantRepository extends JpaRepository<ResourceAccessGrant, Long> {

/** Resolution of the identifier this row wears outside the API boundary. */
Optional<ResourceAccessGrant> findByPublicId(UUID publicId);

/**
* Every grant on one resource — the two rows the judgment needs (the
* requester's own and the workspace-wide one) plus the list surfaces' contents.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import kr.ac.pusan.pickle.access.dto.AddResourceAccessGrantRequest;
Expand Down Expand Up @@ -77,22 +78,25 @@ public ResourceAccessGrantService(List<ResourceTypeAdapter> adapters,

@Transactional(readOnly = true)
public ResourceAccessListResponse list(AuthenticatedUser actor, ResourceType type,
long resourceId) {
UUID resourceId) {
Managed managed = requireManager(actor, type, resourceId);
String workspaceName = workspaceRepository.findById(managed.resource().workspaceId())
.map(Workspace::getName).orElse("");
return ResourceAccessListResponse.of(type, managed.resource(), workspaceName,
views(type, resourceId));
Workspace workspace = workspaceRepository.findById(managed.resource().workspaceId())
.orElse(null);
return ResourceAccessListResponse.of(type, managed.resource(),
workspace == null ? null : workspace.getPublicId(),
workspace == null ? "" : workspace.getName(),
views(type, managed.resource().id()));
}

@Transactional
public ResourceAccessGrantView add(AuthenticatedUser actor, ResourceType type, long resourceId,
public ResourceAccessGrantView add(AuthenticatedUser actor, ResourceType type, UUID resourceId,
AddResourceAccessGrantRequest request, String ip) {
Managed managed = requireManager(actor, type, resourceId);
long id = managed.resource().id();
ResourceAccessGrant grant = request.granteeType() == AccessGranteeType.WORKSPACE
? ResourceAccessGrant.forOwningWorkspace(type, resourceId,
? ResourceAccessGrant.forOwningWorkspace(type, id,
requireWorkspaceWideRole(request.role()))
: ResourceAccessGrant.forUser(type, resourceId,
: ResourceAccessGrant.forUser(type, id,
requireEligibleMember(managed, request.userId()), request.role());
ResourceAccessGrant saved;
try {
Expand All @@ -106,7 +110,7 @@ public ResourceAccessGrantView add(AuthenticatedUser actor, ResourceType type, l

@Transactional
public ResourceAccessGrantView update(AuthenticatedUser actor, ResourceType type,
long resourceId, long grantId, UpdateResourceAccessGrantRequest request, String ip) {
UUID resourceId, UUID grantId, UpdateResourceAccessGrantRequest request, String ip) {
Managed managed = requireManager(actor, type, resourceId);
ResourceAccessGrant grant = requireGrant(managed, grantId);
ResourceRole previous = grant.getRole();
Expand All @@ -118,7 +122,7 @@ public ResourceAccessGrantView update(AuthenticatedUser actor, ResourceType type
}

@Transactional
public void remove(AuthenticatedUser actor, ResourceType type, long resourceId, long grantId,
public void remove(AuthenticatedUser actor, ResourceType type, UUID resourceId, UUID grantId,
String ip) {
Managed managed = requireManager(actor, type, resourceId);
ResourceAccessGrant grant = requireGrant(managed, grantId);
Expand All @@ -127,11 +131,11 @@ public void remove(AuthenticatedUser actor, ResourceType type, long resourceId,
}

/** Managing the list is a resource owner's right, or a workspace owner's standing one. */
private Managed requireManager(AuthenticatedUser actor, ResourceType type, long resourceId) {
private Managed requireManager(AuthenticatedUser actor, ResourceType type, UUID resourceId) {
ResourceTypeAdapter adapter = adapterFor(type);
ResourceIdentity resource = adapter.identify(resourceId)
ResourceIdentity resource = adapter.identifyByPublicId(resourceId)
.orElseThrow(() -> adapter.accessMessages().notFound());
ResourceStanding standing = resolver.standing(type, resourceId, resource.workspaceId(),
ResourceStanding standing = resolver.standing(type, resource.id(), resource.workspaceId(),
actor.id());
if (!standing.manages()) {
// An outsider is not told the resource exists and a member without
Expand All @@ -156,16 +160,20 @@ private ResourceTypeAdapter adapterFor(ResourceType type) {
* rule that keeps the list from disagreeing with the 404 that hides the
* resource from everyone else.
*/
private long requireEligibleMember(Managed managed, Long userId) {
if (userId == null) {
private long requireEligibleMember(Managed managed, UUID publicUserId) {
if (publicUserId == null) {
throw ApiException.validationFailed(List.of(new FieldValidationError("userId",
"대상 사용자를 지정해 주세요.")));
}
// An id no account has is refused as ineligible, not as missing: the
// grantee's existence is not this endpoint's to disclose.
Long userId = userRepository.findByPublicId(publicUserId)
.filter(user -> user.getStatus() == UserStatus.ACTIVE)
.map(kr.ac.pusan.pickle.user.User::getId)
.orElseThrow(() -> managed.messages().granteeIneligible());
boolean member = workspaceMemberRepository
.findByWorkspaceIdAndUserId(managed.resource().workspaceId(), userId).isPresent();
boolean active = userRepository.findById(userId)
.filter(user -> user.getStatus() == UserStatus.ACTIVE).isPresent();
if (!member || !active) {
if (!member) {
throw managed.messages().granteeIneligible();
}
return userId;
Expand All @@ -182,8 +190,8 @@ private ResourceRole requireWorkspaceWideRole(ResourceRole role) {
}

/** A grant id means nothing outside the resource it was issued under. */
private ResourceAccessGrant requireGrant(Managed managed, long grantId) {
return grantRepository.findById(grantId)
private ResourceAccessGrant requireGrant(Managed managed, UUID grantId) {
return grantRepository.findByPublicId(grantId)
.filter(grant -> grant.getResourceType() == managed.adapter().type()
&& grant.getResourceId() == managed.resource().id())
.orElseThrow(() -> new ApiException(HttpStatus.NOT_FOUND,
Expand All @@ -204,12 +212,11 @@ private void audit(AuthenticatedUser actor, Managed managed, GrantChange change,
if (previousRole != null) {
detail.put("previousRole", previousRole.name());
}
long resourceId = managed.resource().id();
auditService.recordAfterCommit(actor.id(), actor.role().name(), names.actionOf(change),
names.targetType(), resourceId, Map.copyOf(detail), ip);
names.targetType(), managed.resource().publicId(), Map.copyOf(detail), ip);
if (opensTheDoorForActor(actor, managed, change, grant)) {
auditService.recordAfterCommit(actor.id(), actor.role().name(), names.breakGlass(),
names.targetType(), resourceId, Map.copyOf(detail), ip);
names.targetType(), managed.resource().publicId(), Map.copyOf(detail), ip);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.UUID;
import kr.ac.pusan.pickle.access.dto.AddResourceAccessGrantRequest;
import kr.ac.pusan.pickle.access.dto.UpdateResourceAccessGrantRequest;
import kr.ac.pusan.pickle.access.dto.ResourceAccessGrantView;
Expand Down Expand Up @@ -47,7 +48,7 @@ public VmAccessGrantController(ResourceAccessGrantService service) {
description = "이 VM의 접근 권한 전체와, 그 목록이 어느 VM의 것인지 알려 주는 최소 정보입니다. VM 소유자와 워크스페이스 소유자만 볼 수 있습니다.")
public ResourceAccessListResponse listVmAccessGrants(
@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long vmId) {
@PathVariable UUID vmId) {
return service.list(principal, ResourceType.VM, vmId);
}

Expand All @@ -60,7 +61,7 @@ public ResourceAccessListResponse listVmAccessGrants(
+ "참여자·열람자까지만 부여할 수 있습니다.")
public ResourceAccessGrantView addVmAccessGrant(
@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long vmId,
@PathVariable UUID vmId,
@Valid @RequestBody AddResourceAccessGrantRequest request,
HttpServletRequest httpRequest) {
return service.add(principal, ResourceType.VM, vmId, request, clientIp(httpRequest));
Expand All @@ -71,8 +72,8 @@ public ResourceAccessGrantView addVmAccessGrant(
@Operation(summary = "접근 권한 등급 변경")
public ResourceAccessGrantView updateVmAccessGrant(
@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long vmId,
@PathVariable long grantId,
@PathVariable UUID vmId,
@PathVariable UUID grantId,
@Valid @RequestBody UpdateResourceAccessGrantRequest request,
HttpServletRequest httpRequest) {
return service.update(principal, ResourceType.VM, vmId, grantId, request,
Expand All @@ -87,8 +88,8 @@ public ResourceAccessGrantView updateVmAccessGrant(
+ "않습니다. 필요하면 비밀번호를 재생성해 주세요.")
public void removeVmAccessGrant(
@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long vmId,
@PathVariable long grantId,
@PathVariable UUID vmId,
@PathVariable UUID grantId,
HttpServletRequest httpRequest) {
service.remove(principal, ResourceType.VM, vmId, grantId, clientIp(httpRequest));
}
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/kr/ac/pusan/pickle/access/VmAccessService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.ac.pusan.pickle.access;

import java.util.UUID;
import kr.ac.pusan.pickle.common.error.ApiException;
import kr.ac.pusan.pickle.resource.VmResourceAdapter;
import kr.ac.pusan.pickle.security.AuthenticatedUser;
Expand Down Expand Up @@ -40,14 +41,14 @@ public VmAccessService(VmRepository vmRepository, ResourceAccessResolver resolve

/** Standing of {@code actor} on {@code vmId}; unknown VM answers 404. */
@Transactional(readOnly = true)
public VmAccess of(AuthenticatedUser actor, long vmId) {
public VmAccess of(AuthenticatedUser actor, UUID vmId) {
return of(vmId, actor.id());
}

/** Standing of one user on {@code vmId}; unknown VM answers 404. */
@Transactional(readOnly = true)
public VmAccess of(long vmId, long userId) {
Vm vm = vmRepository.findById(vmId).orElseThrow(VmAccessService::vmNotFound);
public VmAccess of(UUID vmId, long userId) {
Vm vm = vmRepository.findByPublicId(vmId).orElseThrow(VmAccessService::vmNotFound);
return of(vm, userId);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
import kr.ac.pusan.pickle.access.AccessGranteeType;
import kr.ac.pusan.pickle.access.ResourceRole;
import org.jspecify.annotations.Nullable;
Expand All @@ -11,7 +12,7 @@ public record AddResourceAccessGrantRequest(
@Schema(description = "USER는 지정된 사용자 한 명, WORKSPACE은 소유 워크스페이스 전체")
@NotNull AccessGranteeType granteeType,
@Schema(description = "대상 사용자 id. granteeType이 USER일 때만 보내며, 소유 워크스페이스의 구성원이어야 합니다.")
@Nullable Long userId,
@Nullable UUID userId,
@Schema(description = "부여할 등급. 워크스페이스 전체 항목에는 MEMBER 또는 VIEWER만 지정할 수 있습니다.")
@NotNull ResourceRole role) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.UUID;
import kr.ac.pusan.pickle.access.AccessGranteeType;
import kr.ac.pusan.pickle.access.ResourceAccessGrant;
import kr.ac.pusan.pickle.access.ResourceRole;
Expand All @@ -11,7 +12,7 @@
/** One entry in a VM's access list. */
@Schema(description = "VM 접근 권한 한 건. 대상은 지정된 사용자 한 명이거나 소유 워크스페이스 전체입니다.")
public record ResourceAccessGrantView(
Long id,
UUID id,
@Schema(description = "대상 종류 — USER는 지정된 사용자, WORKSPACE은 소유 워크스페이스 전체")
AccessGranteeType granteeType,
@Schema(description = "대상 사용자. 워크스페이스 전체 항목이면 null입니다.")
Expand All @@ -21,13 +22,13 @@ public record ResourceAccessGrantView(
Instant createdAt) {

@Schema(description = "접근 권한을 가진 사용자")
public record Grantee(Long userId, String name, String email) {
public record Grantee(UUID userId, String name, String email) {
}

public static ResourceAccessGrantView of(ResourceAccessGrant grant, @Nullable User user) {
Grantee grantee = user == null ? null
: new Grantee(user.getId(), user.getName(), user.getEmail());
return new ResourceAccessGrantView(grant.getId(), grant.getGranteeType(), grantee,
: new Grantee(user.getPublicId(), user.getName(), user.getEmail());
return new ResourceAccessGrantView(grant.getPublicId(), grant.getGranteeType(), grantee,
grant.getRole(), grant.getCreatedAt());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.UUID;
import kr.ac.pusan.pickle.access.ResourceType;
import kr.ac.pusan.pickle.resource.ResourceIdentity;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -29,22 +30,22 @@ public record ResourceAccessListResponse(
/** Name, state and owning workspace — the same fields a restricted row shows. */
@Schema(description = "접근 권한이 없는 사람에게도 보이는 범위의 리소스 정보")
public record ResourceBrief(
Long id,
UUID id,
ResourceType type,
String name,
@Schema(description = "표시명. 지정되지 않았으면 null입니다.")
@Nullable String displayName,
@Schema(description = "리소스 종류의 상태값. 종류마다 어휘가 다릅니다.")
String status,
Long workspaceId,
UUID workspaceId,
String workspaceName) {
}

public static ResourceAccessListResponse of(ResourceType type, ResourceIdentity resource,
String workspaceName, List<ResourceAccessGrantView> grants) {
UUID workspaceId, String workspaceName, List<ResourceAccessGrantView> grants) {
return new ResourceAccessListResponse(
new ResourceBrief(resource.id(), type, resource.name(), resource.displayName(),
resource.status(), resource.workspaceId(), workspaceName),
new ResourceBrief(resource.publicId(), type, resource.name(), resource.displayName(),
resource.status(), workspaceId, workspaceName),
grants);
}
}
5 changes: 3 additions & 2 deletions src/main/java/kr/ac/pusan/pickle/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.List;
import java.util.UUID;
import kr.ac.pusan.pickle.admin.dto.CreateOrgRequest;
import kr.ac.pusan.pickle.admin.dto.OrgDetailResponse;
import kr.ac.pusan.pickle.admin.dto.UpdateOrgRequest;
Expand Down Expand Up @@ -62,15 +63,15 @@ public OrgDetailResponse createOrg(

@PatchMapping("/orgs/{orgId}")
public OrgDetailResponse updateOrg(@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long orgId,
@PathVariable UUID orgId,
@Valid @RequestBody UpdateOrgRequest request,
HttpServletRequest httpRequest) {
return adminService.updateOrg(principal, orgId, request, clientIp(httpRequest));
}

@PatchMapping("/users/{userId}")
public UserSummaryResponse updateUser(@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long userId,
@PathVariable UUID userId,
@Valid @RequestBody UpdateUserAdminRequest request,
HttpServletRequest httpRequest) {
return adminService.updateUser(principal, userId, request, clientIp(httpRequest));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.util.UUID;
import kr.ac.pusan.pickle.admin.dto.DriftFindingResponse;
import kr.ac.pusan.pickle.admin.dto.ResolveDriftFindingRequest;
import kr.ac.pusan.pickle.common.web.PageResponse;
Expand Down Expand Up @@ -49,7 +50,7 @@ public PageResponse<DriftFindingResponse> listDriftFindings(
@PostMapping("/{findingId}/resolve")
public DriftFindingResponse resolveDriftFinding(
@AuthenticationPrincipal AuthenticatedUser principal,
@PathVariable long findingId,
@PathVariable UUID findingId,
@Valid @RequestBody(required = false) ResolveDriftFindingRequest request,
HttpServletRequest httpRequest) {
return adminDriftService.resolve(principal, findingId, request, clientIp(httpRequest));
Expand Down
Loading