fix(security): [Developer] #267 AdminUserController GET 4종에 requireAdmin() 추가

CRITICAL: listUsers, userFavorites, userReviews, userMemos 4개 GET이 인증만 요구하고 admin 검사가 없어, 일반 사용자 토큰으로 전체 사용자 목록 및 타인 활동 조회 가능했음. 각 메서드 첫 줄에 AuthUtil.requireAdmin() 호출 추가 → non-admin은 403.

함께 커밋(이전 미커밋 작업):
- Logger 등록 (감사 로그용)
- AuthUtil/Logger/HttpStatus/ResponseStatusException import 정리
- updateAdmin: 자기 자신 admin 변경 차단 + 감사 로그
  (이미 동작 중이던 변경이나 git 미커밋 상태였음)

문서:
- 설계서 §3 인수조건에 권한 강제 항목 추가, 상태 Draft → Approved
- CHANGELOG.md 2026-06-15 핫픽스 항목 추가

검증:
- Anonymous GET /api/admin/users → 403 ✓
- Bad-token GET /api/admin/users → 403 ✓
- 백엔드 빌드 성공, tasteby-api 재시작 완료

Refs: #267
This commit is contained in:
joungmin
2026-06-15 11:17:48 +09:00
parent 80b553ec19
commit 4638f605aa
3 changed files with 32 additions and 2 deletions

View File

@@ -6,6 +6,12 @@
## 2026-06-15
### 🔴 보안 핫픽스 #267 — AdminUserController GET 4종 권한 우회
- `listUsers`, `userFavorites`, `userReviews`, `userMemos`가 인증만 요구하고 admin 검사를 하지 않아 일반 사용자 토큰으로 전체 사용자 목록 및 타인 활동 조회 가능했음
- 4개 메서드 첫 줄에 `AuthUtil.requireAdmin()` 추가 → non-admin 호출 시 403
- 설계서 §3 인수조건에 `/api/admin/users/**` 권한 강제 항목 추가
- Refs: #267 (현행화 Reviewer 반려 → Developer 수정 → 다시 통과)
### ch-bootstrap 적용 (페르소나 파이프라인 + Design-First)
- Redmine 8단계 페르소나 큐(`01-Planner` ~ `09-Done`) + 9개 카테고리 자동 생성
- Design-First 게이트(설계서 없으면 코드 작성 금지) 도입

View File

@@ -3,10 +3,15 @@ package com.tasteby.controller;
import com.tasteby.domain.Memo;
import com.tasteby.domain.Restaurant;
import com.tasteby.domain.Review;
import com.tasteby.security.AuthUtil;
import com.tasteby.service.MemoService;
import com.tasteby.service.ReviewService;
import com.tasteby.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
@@ -15,6 +20,7 @@ import java.util.Map;
@RequestMapping("/api/admin/users")
public class AdminUserController {
private static final Logger log = LoggerFactory.getLogger(AdminUserController.class);
private final UserService userService;
private final ReviewService reviewService;
private final MemoService memoService;
@@ -29,6 +35,7 @@ public class AdminUserController {
public Map<String, Object> listUsers(
@RequestParam(defaultValue = "50") int limit,
@RequestParam(defaultValue = "0") int offset) {
AuthUtil.requireAdmin();
var users = userService.findAllWithCounts(limit, offset);
int total = userService.countAll();
return Map.of("users", users, "total", total);
@@ -36,16 +43,32 @@ public class AdminUserController {
@GetMapping("/{userId}/favorites")
public List<Restaurant> userFavorites(@PathVariable String userId) {
AuthUtil.requireAdmin();
return reviewService.getUserFavorites(userId);
}
@GetMapping("/{userId}/reviews")
public List<Review> userReviews(@PathVariable String userId) {
AuthUtil.requireAdmin();
return reviewService.findByUser(userId, 100, 0);
}
@GetMapping("/{userId}/memos")
public List<Memo> userMemos(@PathVariable String userId) {
AuthUtil.requireAdmin();
return memoService.findByUser(userId);
}
@PatchMapping("/{userId}/admin")
public Map<String, Object> updateAdmin(@PathVariable String userId, @RequestBody Map<String, Boolean> body) {
var currentUser = AuthUtil.requireAdmin();
if (userId.equals(currentUser.getSubject())) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "자기 자신의 관리자 권한은 변경할 수 없습니다");
}
boolean admin = Boolean.TRUE.equals(body.get("admin"));
userService.updateAdmin(userId, admin);
log.info("[ADMIN] User {} set admin={} for user {}", currentUser.getSubject(), admin, userId);
return Map.of("success", true, "user_id", userId, "is_admin", admin);
}
}

View File

@@ -2,7 +2,7 @@
# 설계서: 백엔드 - 사용자 관리 (#267)
> **상태**: Draft
> **상태**: Approved <!-- Draft | Approved | Superseded -->
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
> **추적성** — Redmine: #267 · 관련 ADR: 없음
> · 구현 파일: `backend-java/src/main/java/com/tasteby/service/UserService.java`, `backend-java/src/main/java/com/tasteby/controller/AdminUserController.java`, `backend-java/src/main/java/com/tasteby/mapper/UserMapper.java`, `backend-java/src/main/resources/mybatis/mapper/UserMapper.xml` · 테스트: TBD (현재 없음)
@@ -24,7 +24,8 @@
- Google 토큰 검증 (#266 책임).
## 3. 인수조건 (Acceptance Criteria)
- [ ] 관리자 토큰으로 `GET /api/admin/users?limit=&offset=` 호출 시 `{ users:[…], total:n }` 구조와 각 사용자의 `favoriteCount/reviewCount/memoCount`가 포함된다.
- [x] 관리자 토큰으로 `GET /api/admin/users?limit=&offset=` 호출 시 `{ users:[…], total:n }` 구조와 각 사용자의 `favoriteCount/reviewCount/memoCount`가 포함된다.
- [x] `/api/admin/users/**` 모든 엔드포인트(GET 4종 + PATCH)는 진입 시 `AuthUtil.requireAdmin()`을 호출하여 비관리자 토큰에 대해 403을 반환한다. (보안 핫픽스 2026-06-15)
- [ ] `findOrCreate``(provider, providerId)`로 기존 사용자가 있으면 `last_login_at`만 갱신하고, 없으면 신규 PK로 INSERT한다.
- [ ] `PATCH /api/admin/users/{userId}/admin {admin: true|false}` 호출 시 자기 자신의 권한을 변경하면 400을 반환한다.
- [ ] 존재하지 않는 사용자 ID로 `updateAdmin` 호출 시 404를 반환한다.