feat(sheets): 사용자 OAuth 리프레시 토큰 인증 지원
조직 정책(iam.disableServiceAccountKeyCreation)으로 서비스 계정 키를 만들 수 없는 환경 대응. 서비스 계정이 설정돼 있으면 그쪽을 먼저 쓰고, 없으면 GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN + client id/secret 으로 인증한다. (client id/secret 미지정 시 로그인용 GOOGLE_CLIENT_ID/SECRET 재사용) scripts/google_sheets_authorize.py: 데스크톱 앱 OAuth 클라이언트로 1회 동의해 refresh token 을 발급받는 스크립트. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Google 스프레드시트 쓰기용 refresh token 1회 발급 스크립트.
|
||||
|
||||
조직 정책(`iam.disableServiceAccountKeyCreation`)으로 서비스 계정 키를 만들 수 없을 때
|
||||
사용자 계정 OAuth 로 서버가 시트에 기록하도록 하기 위한 토큰을 받는다.
|
||||
|
||||
사전 준비 (Google Cloud Console)
|
||||
1. API 및 서비스 → 라이브러리 → "Google Sheets API" 사용
|
||||
2. API 및 서비스 → OAuth 동의 화면 구성(내부 또는 외부)
|
||||
3. 사용자 인증 정보 → 사용자 인증 정보 만들기 → **OAuth 클라이언트 ID**
|
||||
→ 애플리케이션 유형 **데스크톱 앱** → 클라이언트 ID / 보안 비밀 확인
|
||||
|
||||
실행 (개발 PC, 브라우저 있는 환경에서)
|
||||
python scripts/google_sheets_authorize.py --client-id <ID> --client-secret <SECRET>
|
||||
또는 환경변수로:
|
||||
GOOGLE_SHEETS_OAUTH_CLIENT_ID / GOOGLE_SHEETS_OAUTH_CLIENT_SECRET
|
||||
|
||||
브라우저가 열리면 **시트를 편집할 수 있는 계정**으로 로그인·동의한다.
|
||||
마지막에 출력되는 refresh token 을 서버 `.env` 의
|
||||
`GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 에 넣는다.
|
||||
|
||||
주의
|
||||
- 출력된 refresh token 은 비밀값이다. Git·메신저에 남기지 말 것.
|
||||
- 토큰은 발급한 사용자 권한으로 동작한다. 그 사용자가 대상 문서를 편집할 수
|
||||
있어야 하며, 계정 비밀번호 변경·토큰 폐기 시 다시 발급해야 한다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
SCOPES = ["https://www.googleapis.com/auth/spreadsheets"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Google Sheets refresh token 발급")
|
||||
parser.add_argument("--client-id", default=os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_ID", ""))
|
||||
parser.add_argument(
|
||||
"--client-secret", default=os.getenv("GOOGLE_SHEETS_OAUTH_CLIENT_SECRET", "")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port", type=int, default=0, help="로컬 콜백 포트(기본: 임의 포트)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
client_id = (args.client_id or "").strip()
|
||||
client_secret = (args.client_secret or "").strip()
|
||||
if not client_id or not client_secret:
|
||||
print("client id / secret 이 필요합니다. --client-id / --client-secret 또는 환경변수로 넣으세요.")
|
||||
return 2
|
||||
|
||||
try:
|
||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||
except ImportError:
|
||||
print("google-auth-oauthlib 가 필요합니다: pip install google-auth-oauthlib")
|
||||
return 2
|
||||
|
||||
flow = InstalledAppFlow.from_client_config(
|
||||
{
|
||||
"installed": {
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"redirect_uris": ["http://localhost"],
|
||||
}
|
||||
},
|
||||
scopes=SCOPES,
|
||||
)
|
||||
# prompt=consent 를 줘야 refresh token 이 항상 내려온다.
|
||||
creds = flow.run_local_server(port=args.port, prompt="consent", access_type="offline")
|
||||
|
||||
if not creds.refresh_token:
|
||||
print("refresh token 을 받지 못했습니다. 계정의 기존 권한을 해제하고 다시 시도하세요.")
|
||||
return 1
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("서버 .env 에 아래 값을 넣으세요 (비밀값 — 공유 금지)")
|
||||
print("=" * 70)
|
||||
print(f"GOOGLE_SHEETS_OAUTH_CLIENT_ID={client_id}")
|
||||
print("GOOGLE_SHEETS_OAUTH_CLIENT_SECRET=<위에서 쓴 보안 비밀>")
|
||||
print(f"GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN={creds.refresh_token}")
|
||||
print("=" * 70)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user