Skip to content

Commit 8e1e241

Browse files
authored
fix: Sheet API 요청자 접근 권한 검증 추가
* fix: integrated 시트 요청자 접근 권한 검증 추가 * fix: integrated 시트 권한 검증 보완 * fix: integrated 시트 서비스 계정 접근 재검증 추가 * fix: integrated 시트 Drive OAuth 미연결 우회 차단 * test: integrated 시트 OAuth 미연결 중단 검증 추가 * test: integrated 시트 검증 테스트 중복 정리
1 parent a8153df commit 8e1e241

5 files changed

Lines changed: 426 additions & 73 deletions

File tree

src/main/java/gg/agit/konect/domain/club/service/ClubSheetIntegratedService.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ public SheetImportResponse analyzeAndImportPreMembers(
2323
clubPermissionValidator.validateManagerAccess(clubId, requesterId);
2424

2525
String spreadsheetId = SpreadsheetUrlParser.extractId(spreadsheetUrl);
26-
// OAuth 미연결이면 건너뛰고 계속 진행한다. Drive 초기화/인증 오류는 예외로 전파한다.
27-
googleSheetPermissionService.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
26+
// integrated 등록은 요청자 Google Drive OAuth 연결을 전제로 한다.
27+
// 연결된 계정이 실제 시트 접근 권한을 가지는지 검증한 뒤 서비스 계정 권한을 맞춘다.
28+
googleSheetPermissionService.validateRequesterAccessAndTryGrantServiceAccountWriterAccess(
29+
requesterId,
30+
spreadsheetId
31+
);
2832

2933
SheetHeaderMapper.SheetAnalysisResult analysis =
3034
sheetHeaderMapper.analyzeAllSheets(spreadsheetId);

src/main/java/gg/agit/konect/domain/club/service/GoogleDrivePermissionHelper.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ static List<Permission> listAllPermissions(Drive driveService, String fileId) th
9494

9595
do {
9696
Drive.Permissions.List request = driveService.permissions().list(fileId)
97-
.setFields(PERMISSION_FIELDS);
97+
.setFields(PERMISSION_FIELDS)
98+
.setSupportsAllDrives(true);
9899
if (nextPageToken != null) {
99100
request.setPageToken(nextPageToken);
100101
}
@@ -142,6 +143,7 @@ private static PermissionApplyStatus applyServiceAccountPermission(
142143

143144
userDriveService.permissions().create(fileId, permission)
144145
.setSendNotificationEmail(false)
146+
.setSupportsAllDrives(true)
145147
.execute();
146148
log.info(
147149
"Service account {} access granted. fileId={}, email={}",
@@ -165,6 +167,7 @@ private static PermissionApplyStatus applyServiceAccountPermission(
165167

166168
Permission updatedPermission = new Permission().setRole(targetRole);
167169
userDriveService.permissions().update(fileId, existingPermission.getId(), updatedPermission)
170+
.setSupportsAllDrives(true)
168171
.execute();
169172
log.info(
170173
"Service account permission upgraded. fileId={}, fromRole={}, toRole={}, email={}",

src/main/java/gg/agit/konect/domain/club/service/GoogleSheetPermissionService.java

Lines changed: 143 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.util.StringUtils;
88

99
import com.google.api.services.drive.Drive;
10+
import com.google.api.services.drive.model.File;
1011
import com.google.auth.oauth2.ServiceAccountCredentials;
1112

1213
import gg.agit.konect.domain.user.enums.Provider;
@@ -23,15 +24,25 @@
2324
public class GoogleSheetPermissionService {
2425

2526
private final ServiceAccountCredentials serviceAccountCredentials;
27+
private final Drive googleDriveService;
2628
private final GoogleSheetsConfig googleSheetsConfig;
2729
private final UserOAuthAccountRepository userOAuthAccountRepository;
2830

31+
public void validateRequesterAccessAndTryGrantServiceAccountWriterAccess(
32+
Integer requesterId,
33+
String spreadsheetId
34+
) {
35+
String refreshToken = requireRefreshToken(requesterId);
36+
Drive userDriveService = buildUserDriveService(refreshToken, requesterId);
37+
validateRequesterSpreadsheetAccess(userDriveService, requesterId, spreadsheetId);
38+
boolean granted = tryGrantServiceAccountWriterAccess(userDriveService, requesterId, spreadsheetId);
39+
if (!granted) {
40+
requireServiceAccountSpreadsheetAccess(spreadsheetId, requesterId);
41+
}
42+
}
43+
2944
public boolean tryGrantServiceAccountWriterAccess(Integer requesterId, String spreadsheetId) {
30-
String refreshToken = userOAuthAccountRepository
31-
.findByUserIdAndProvider(requesterId, Provider.GOOGLE)
32-
.map(account -> account.getGoogleDriveRefreshToken())
33-
.filter(StringUtils::hasText)
34-
.orElse(null);
45+
String refreshToken = resolveRefreshToken(requesterId);
3546

3647
if (refreshToken == null) {
3748
log.warn(
@@ -41,14 +52,35 @@ public boolean tryGrantServiceAccountWriterAccess(Integer requesterId, String sp
4152
return false;
4253
}
4354

44-
Drive userDriveService;
45-
try {
46-
userDriveService = googleSheetsConfig.buildUserDriveService(refreshToken);
47-
} catch (IOException | GeneralSecurityException e) {
48-
log.error("Failed to build user Drive service. requesterId={}", requesterId, e);
49-
throw CustomException.of(ApiResponseCode.FAILED_INIT_GOOGLE_DRIVE);
50-
}
55+
Drive userDriveService = buildUserDriveService(refreshToken, requesterId);
56+
return tryGrantServiceAccountWriterAccess(userDriveService, requesterId, spreadsheetId);
57+
}
58+
59+
private String requireRefreshToken(Integer requesterId) {
60+
return userOAuthAccountRepository.findByUserIdAndProvider(requesterId, Provider.GOOGLE)
61+
.map(account -> account.getGoogleDriveRefreshToken())
62+
.filter(StringUtils::hasText)
63+
.orElseThrow(() -> {
64+
log.warn(
65+
"Rejecting spreadsheet registration because Google Drive OAuth is not connected. requesterId={}",
66+
requesterId
67+
);
68+
return CustomException.of(ApiResponseCode.NOT_FOUND_GOOGLE_DRIVE_AUTH);
69+
});
70+
}
71+
72+
private String resolveRefreshToken(Integer requesterId) {
73+
return userOAuthAccountRepository.findByUserIdAndProvider(requesterId, Provider.GOOGLE)
74+
.map(account -> account.getGoogleDriveRefreshToken())
75+
.filter(StringUtils::hasText)
76+
.orElse(null);
77+
}
5178

79+
private boolean tryGrantServiceAccountWriterAccess(
80+
Drive userDriveService,
81+
Integer requesterId,
82+
String spreadsheetId
83+
) {
5284
try {
5385
GoogleDrivePermissionHelper.ensureServiceAccountPermission(
5486
userDriveService,
@@ -91,6 +123,105 @@ public boolean tryGrantServiceAccountWriterAccess(Integer requesterId, String sp
91123
}
92124
}
93125

126+
private Drive buildUserDriveService(String refreshToken, Integer requesterId) {
127+
try {
128+
return googleSheetsConfig.buildUserDriveService(refreshToken);
129+
} catch (IOException | GeneralSecurityException e) {
130+
log.error("Failed to build user Drive service. requesterId={}", requesterId, e);
131+
throw CustomException.of(ApiResponseCode.FAILED_INIT_GOOGLE_DRIVE);
132+
}
133+
}
134+
135+
private void validateRequesterSpreadsheetAccess(
136+
Drive userDriveService,
137+
Integer requesterId,
138+
String spreadsheetId
139+
) {
140+
try {
141+
File file = userDriveService.files().get(spreadsheetId)
142+
.setFields("id")
143+
.setSupportsAllDrives(true)
144+
.execute();
145+
if (file == null || !StringUtils.hasText(file.getId())) {
146+
throw GoogleSheetApiExceptionHelper.accessDenied();
147+
}
148+
} catch (IOException e) {
149+
if (GoogleSheetApiExceptionHelper.isInvalidGrant(e)) {
150+
log.warn(
151+
"Google Drive OAuth token is invalid while validating spreadsheet access. requesterId={}, "
152+
+ "spreadsheetId={}, cause={}",
153+
requesterId,
154+
spreadsheetId,
155+
GoogleSheetApiExceptionHelper.extractDetail(e)
156+
);
157+
throw GoogleSheetApiExceptionHelper.invalidGoogleDriveAuth(e);
158+
}
159+
160+
if (GoogleSheetApiExceptionHelper.isAuthFailure(e)) {
161+
log.warn(
162+
"Google Drive OAuth auth failure while validating spreadsheet access. requesterId={}, "
163+
+ "spreadsheetId={}, cause={}",
164+
requesterId,
165+
spreadsheetId,
166+
GoogleSheetApiExceptionHelper.extractDetail(e)
167+
);
168+
throw GoogleSheetApiExceptionHelper.invalidGoogleDriveAuth(e);
169+
}
170+
171+
if (GoogleSheetApiExceptionHelper.isAccessDenied(e)
172+
|| GoogleSheetApiExceptionHelper.isNotFound(e)) {
173+
log.warn(
174+
"Requester has no spreadsheet access. requesterId={}, spreadsheetId={}, cause={}",
175+
requesterId,
176+
spreadsheetId,
177+
e.getMessage()
178+
);
179+
throw GoogleSheetApiExceptionHelper.accessDenied();
180+
}
181+
182+
log.error(
183+
"Unexpected error while validating requester spreadsheet access. requesterId={}, spreadsheetId={}",
184+
requesterId,
185+
spreadsheetId,
186+
e
187+
);
188+
throw CustomException.of(ApiResponseCode.FAILED_SYNC_GOOGLE_SHEET);
189+
}
190+
}
191+
192+
private void requireServiceAccountSpreadsheetAccess(String spreadsheetId, Integer requesterId) {
193+
try {
194+
File file = googleDriveService.files().get(spreadsheetId)
195+
.setFields("id")
196+
.setSupportsAllDrives(true)
197+
.execute();
198+
if (file == null || !StringUtils.hasText(file.getId())) {
199+
throw GoogleSheetApiExceptionHelper.accessDenied();
200+
}
201+
} catch (IOException e) {
202+
if (GoogleSheetApiExceptionHelper.isAccessDenied(e)
203+
|| GoogleSheetApiExceptionHelper.isNotFound(e)) {
204+
log.warn(
205+
"Service account has no spreadsheet access after auto-share failed. requesterId={}, "
206+
+ "spreadsheetId={}, cause={}",
207+
requesterId,
208+
spreadsheetId,
209+
e.getMessage()
210+
);
211+
throw GoogleSheetApiExceptionHelper.accessDenied();
212+
}
213+
214+
log.error(
215+
"Unexpected error while re-checking service account spreadsheet access. requesterId={}, "
216+
+ "spreadsheetId={}",
217+
requesterId,
218+
spreadsheetId,
219+
e
220+
);
221+
throw CustomException.of(ApiResponseCode.FAILED_SYNC_GOOGLE_SHEET);
222+
}
223+
}
224+
94225
private String getServiceAccountEmail() {
95226
return serviceAccountCredentials.getClientEmail();
96227
}

src/test/java/gg/agit/konect/domain/club/service/ClubSheetIntegratedServiceTest.java

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55
import static org.mockito.BDDMockito.given;
6+
import static org.mockito.BDDMockito.willThrow;
67
import static org.mockito.Mockito.inOrder;
78
import static org.mockito.Mockito.verifyNoInteractions;
89

@@ -53,8 +54,6 @@ void analyzeAndImportPreMembersSuccess() {
5354
new SheetHeaderMapper.SheetAnalysisResult(SheetColumnMapping.defaultMapping(), null, null);
5455
SheetImportResponse expected = SheetImportResponse.of(3, 1, List.of("warn"));
5556

56-
given(googleSheetPermissionService.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId))
57-
.willReturn(true);
5857
given(sheetHeaderMapper.analyzeAllSheets(spreadsheetId)).willReturn(analysis);
5958
given(sheetImportService.importPreMembersFromSheet(
6059
clubId,
@@ -81,7 +80,7 @@ void analyzeAndImportPreMembersSuccess() {
8180
);
8281
inOrder.verify(clubPermissionValidator).validateManagerAccess(clubId, requesterId);
8382
inOrder.verify(googleSheetPermissionService)
84-
.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
83+
.validateRequesterAccessAndTryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
8584
inOrder.verify(sheetHeaderMapper).analyzeAllSheets(spreadsheetId);
8685
inOrder.verify(clubMemberSheetService).updateSheetId(
8786
clubId,
@@ -99,76 +98,66 @@ void analyzeAndImportPreMembersSuccess() {
9998
}
10099

101100
@Test
102-
@DisplayName("자동 권한 부여가 실패해도 기존 공유 권한으로 가져오기를 계속 시도한다")
103-
void analyzeAndImportPreMembersContinuesWhenAutoGrantFails() {
101+
@DisplayName("자동 권한 부여 중 예외가 발생하면 후속 시트 작업을 진행하지 않는다")
102+
void analyzeAndImportPreMembersStopsWhenAutoGrantThrowsException() {
104103
// given
105104
Integer clubId = 1;
106105
Integer requesterId = 2;
107106
String spreadsheetUrl =
108107
"https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/edit";
109108
String spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms";
110-
SheetHeaderMapper.SheetAnalysisResult analysis =
111-
new SheetHeaderMapper.SheetAnalysisResult(SheetColumnMapping.defaultMapping(), null, null);
112-
SheetImportResponse expected = SheetImportResponse.of(1, 0, List.of());
109+
CustomException expected = CustomException.of(ApiResponseCode.FAILED_INIT_GOOGLE_DRIVE);
113110

114-
given(googleSheetPermissionService.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId))
115-
.willReturn(false);
116-
given(sheetHeaderMapper.analyzeAllSheets(spreadsheetId)).willReturn(analysis);
117-
given(sheetImportService.importPreMembersFromSheet(
118-
clubId,
119-
requesterId,
120-
spreadsheetId,
121-
analysis.memberListMapping()
122-
))
123-
.willReturn(expected);
111+
willThrow(expected).given(googleSheetPermissionService)
112+
.validateRequesterAccessAndTryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
124113

125-
// when
126-
SheetImportResponse actual = clubSheetIntegratedService.analyzeAndImportPreMembers(
114+
// when & then
115+
assertThatThrownBy(() -> clubSheetIntegratedService.analyzeAndImportPreMembers(
127116
clubId,
128117
requesterId,
129118
spreadsheetUrl
130-
);
119+
))
120+
.isSameAs(expected);
121+
verifyNoInteractions(sheetHeaderMapper, clubMemberSheetService, sheetImportService);
122+
}
131123

132-
// then
133-
InOrder inOrder = inOrder(
134-
clubPermissionValidator,
135-
googleSheetPermissionService,
136-
sheetHeaderMapper,
137-
clubMemberSheetService,
138-
sheetImportService
139-
);
140-
inOrder.verify(clubPermissionValidator).validateManagerAccess(clubId, requesterId);
141-
inOrder.verify(googleSheetPermissionService)
142-
.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
143-
inOrder.verify(sheetHeaderMapper).analyzeAllSheets(spreadsheetId);
144-
inOrder.verify(clubMemberSheetService).updateSheetId(
145-
clubId,
146-
requesterId,
147-
spreadsheetId,
148-
analysis
149-
);
150-
inOrder.verify(sheetImportService).importPreMembersFromSheet(
124+
@Test
125+
@DisplayName("요청자 계정이 시트 접근 권한이 없으면 후속 시트 작업을 진행하지 않는다")
126+
void analyzeAndImportPreMembersStopsWhenRequesterHasNoSpreadsheetAccess() {
127+
// given
128+
Integer clubId = 1;
129+
Integer requesterId = 2;
130+
String spreadsheetUrl =
131+
"https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/edit";
132+
String spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms";
133+
CustomException expected = CustomException.of(ApiResponseCode.FORBIDDEN_GOOGLE_SHEET_ACCESS);
134+
135+
willThrow(expected).given(googleSheetPermissionService)
136+
.validateRequesterAccessAndTryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
137+
138+
// when & then
139+
assertThatThrownBy(() -> clubSheetIntegratedService.analyzeAndImportPreMembers(
151140
clubId,
152141
requesterId,
153-
spreadsheetId,
154-
analysis.memberListMapping()
155-
);
156-
assertThat(actual).isEqualTo(expected);
142+
spreadsheetUrl
143+
))
144+
.isSameAs(expected);
145+
verifyNoInteractions(sheetHeaderMapper, clubMemberSheetService, sheetImportService);
157146
}
158147

159148
@Test
160-
@DisplayName("자동 권한 부여 중 예외가 발생하면 후속 시트 작업을 진행하지 않는다")
161-
void analyzeAndImportPreMembersStopsWhenAutoGrantThrowsException() {
149+
@DisplayName("Drive OAuth가 연결되지 않으면 후속 시트 작업을 진행하지 않는다")
150+
void analyzeAndImportPreMembersStopsWhenGoogleDriveOAuthIsNotConnected() {
162151
// given
163152
Integer clubId = 1;
164153
Integer requesterId = 2;
165154
String spreadsheetUrl =
166155
"https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/edit";
167156
String spreadsheetId = "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms";
168-
CustomException expected = CustomException.of(ApiResponseCode.FAILED_INIT_GOOGLE_DRIVE);
157+
CustomException expected = CustomException.of(ApiResponseCode.NOT_FOUND_GOOGLE_DRIVE_AUTH);
169158

170-
given(googleSheetPermissionService.tryGrantServiceAccountWriterAccess(requesterId, spreadsheetId))
171-
.willThrow(expected);
159+
willThrow(expected).given(googleSheetPermissionService)
160+
.validateRequesterAccessAndTryGrantServiceAccountWriterAccess(requesterId, spreadsheetId);
172161

173162
// when & then
174163
assertThatThrownBy(() -> clubSheetIntegratedService.analyzeAndImportPreMembers(

0 commit comments

Comments
 (0)