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:
@@ -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 게이트(설계서 없으면 코드 작성 금지) 도입
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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를 반환한다.
|
||||
|
||||
Reference in New Issue
Block a user