복합 ID(Composite Identity)
GitLab v19.4요약
보안상의 이유로, GitLab 플랫폼에서 쓰기 작업을 수행하는 AI 생성 활동에는 복합 ID를 사용해야 합니다. 복합 ID를 사용하는 기능은 다음과 같습니다. 복합 ID 토큰을 생성하려면 다음이 필요합니다. 기본 토큰 소유자인 서비스 계정 사용자가 있어야 합니다.
보안상의 이유로, GitLab 플랫폼에서 쓰기 작업을 수행하는 AI 생성 활동에는 복합 ID를 사용해야 합니다.
복합 ID를 사용하는 기능은 다음과 같습니다.
사전 요구 사항#
복합 ID 토큰을 생성하려면 다음이 필요합니다.
-
기본 토큰 소유자인 서비스 계정 사용자가 있어야 합니다.
- 인스턴스 전체 범위이거나 그룹 범위일 수 있습니다. GitLab 이 만든 기능에는 인스턴스 전체 계정이 일반적이며, 고객 에이전트 플로에는 범위를 좁게 제한한 계정을 사용해야 합니다.
- 특정 권한을 가진 사용자만 서비스 계정을 생성할 수 있습니다. 계정을 어디에서 어떻게 생성할지 그에 맞게 계획합니다.
- 서비스 계정은 여러 기능에서 공유할 수 있지만, 같은 계정을 공유하면 UI에서 작업을 구분하기 어렵습니다(예: 동일한 사용자가 MR을 생성하고 동시에 리뷰한 것처럼 보일 수 있습니다).
-
가용성 및 라이선스:
- 서비스 계정은 모든 티어에서 사용할 수 있으며 GitLab Free에는 제한이 있습니다. 자세한 내용은 서비스 계정을 참고합니다.
- 복합 ID에는 GitLab Premium 또는 Ultimate 라이선스가 필요합니다.
-
서비스 계정의
composite_identity_enforced가true로 설정되어 있어야 합니다.- 이 설정은 서비스 계정 생성 UI에서는 제공되지 않으며 프로그래밍 방식으로 구성해야 합니다.
-
복합 ID에 사용하는 OAuth 애플리케이션은
user:*동적 범위를 활성화해야 합니다.- 이 범위는 OAuth 애플리케이션 UI에서는 제공되지 않으며 프로그래밍 방식으로 구성해야 합니다.
복합 ID 토큰 생성 방법#
OAuth 토큰만 지원됩니다.
- 서비스 계정은 로그인할 수 없는 봇 사용자이므로, 일반적인 인증 코드 플로(브라우저 동의)는 동작하지 않습니다.
- 서드파티 서비스와 연동하는 경우
- 서비스 계정과 OAuth 앱에 대한 OAuth 권한 부여를 수동으로 생성합니다. Amazon Q 예시를 참고합니다.
- 권한 부여의 범위에 AI 요청을 시작한 사람 사용자에 대한 구체적인 동적 범위가
user:$ID형식(예:user:123)으로 포함되어 있는지 확인합니다. 필요에 따라 다른 범위(예:api)도 포함합니다. https://gitlab.example.com/oauth/token을 통해 권한 부여를 액세스 토큰으로 교환합니다.
- 서드파티 서비스와 연동하지 않는 경우
- 액세스 권한 부여를 건너뛰고 OAuth 액세스 토큰을 직접 생성할 수 있습니다. 이때 범위에
user:$ID와 필요한 기본 범위가 포함되어야 합니다. grant_type=refresh_token을 사용해 표준https://gitlab.example.com/oauth/token>엔드포인트로 토큰을 갱신합니다.
- 액세스 권한 부여를 건너뛰고 OAuth 액세스 토큰을 직접 생성할 수 있습니다. 이때 범위에
- 반환된 액세스 토큰은 서비스 계정에 속하지만 범위에
user:$ID를 담고 있습니다. 표준 OAuth 액세스 토큰과 동일하게 갱신됩니다.
최소 예시#
- OAuth 애플리케이션을 생성합니다(이 단계는 별도의 OAuth 앱이 필요할 때만 수행합니다. GitLab Duo Workflow의 기본 OAuth 애플리케이션과 서비스 계정은
ee/app/services/ai/duo_workflows/onboarding_service.rb를 호출해 생성됩니다).
# Rails console
app = Authn::OauthApplication.new(
name: "Composite Identity App",
redirect_uri: Gitlab::Routing.url_helpers.root_url, # unused but cannot be nil
scopes: ::Gitlab::Auth::AI_WORKFLOW_SCOPES + [::Gitlab::Auth::DYNAMIC_USER],
trusted: false,
confidential: false # public client (no secret required)
)
app.save!
- 서비스 계정과 사람 사용자에 대한 권한 부여를 생성합니다.
# Assuming you want to create a composite OAuth token for the GitLab Duo Workflow OAuth application and service account + root user in your GDK.
org = Organizations::Organization.default_organization
user = User.first
oauth_token_service = Ai::DuoWorkflows::CreateCompositeOauthAccessTokenService.new(
current_user: user,
organization: org,
).execute
oauth_token_service.payload[:oauth_access_token].plaintext_token
인가 평가 방법#
복합 ID 토큰으로 이루어진 요청은 다음 두 조건이 모두 참일 때만 인가됩니다.
- 서비스 계정이 해당 리소스에 접근할 수 있습니다.
- 토큰 범위의
user:$ID로 식별되는 사람 사용자가 해당 리소스에 접근할 수 있습니다.
하나의 요청이 연결할 수 있는 ID 수#
요청은 두 가지 이유 중 하나로 복합 ID를 연결하며, 허용되는 연결 수는 이유마다 다릅니다.
:authentication연결은 최대 하나입니다. 이 연결은 요청이 어떤 주체로 동작하는지 기록합니다. 두 번째 서비스 계정이 이를 차지하려고 하면Gitlab::Auth::Identity::TooManyIdentitiesLinkedError가 발생합니다.:permission_check연결은 개수에 제한이 없습니다. 사람이 하나의 작업에서 여러 AI 서비스 계정을 지정할 수 있습니다. 예를 들어 하나의 퀵 액션으로 두 AI 리뷰어에게 리뷰를 요청하는 경우입니다.Ability가 각 계정의 리소스 접근 권한을 확인할 수 있도록 계정마다 연결이 만들어집니다.
여러 ID가 연결되면 Gitlab::Auth::Identity.currently_linked는 인증된 ID를 반환합니다. 요청에 인증된 ID가 없으면 가장 최근에 연결된 ID를 반환하므로, 특정 서비스 계정을 위해 시작된 작업이 그 계정을 Gitaly, Workhorse, 백그라운드 job으로 전파합니다. 백그라운드 job은 ID를 하나만 가지므로, 특정 서비스 계정으로 동작해야 하는 코드는 작업을 시작하기 직전에 그 계정을 연결해야 합니다.
요청 컨텍스트와 current_user#
요청에 복합 ID OAuth 토큰이 포함되면, Rails 요청 컨텍스트는 current_user를 user:$ID 범위에서 추출한 사람 사용자로 덮어씁니다. 토큰 자체는 여전히 서비스 계정에 속하지만, 요청을 시작한 사용자가 현재 사용자로 간주됩니다. 이는 다음을 의미합니다.
current_user에 의존하는 모든 코드는 사람 사용자로 실행됩니다.- 귀속에 사용할 올바른 액터를 결정하려면
resolve_composite_identity_actor를 사용합니다. 결과는 현재 요청에서 ID가 어떻게 연결되었는지에 따라 달라집니다.
올바른 액터에 작업 귀속하기#
모든 쓰기 작업에서는 항상 resolve_composite_identity_actor로 액터를 결정합니다. 컨텍스트를 직접 판단할 필요는 없으며, 요청 경계에서 자동으로 설정됩니다.
actor = Gitlab::Auth::Identity.resolve_composite_identity_actor(current_user)
작성자를 설정하는 모든 지점(예: 노트, 시스템 노트, 파이프라인 사용자 컨텍스트)에서 반환된 actor를 사용합니다.
이 메서드는 상황에 맞는 올바른 액터를 반환합니다.
- 서비스 계정이 요청한 경우(
user:$ID범위를 가진 OAuth 토큰 또는 CI job): 내부적으로:authentication컨텍스트로 표시됩니다. 서비스 계정을 반환합니다. 이때 SA가 액터이며 SA에 귀속되어야 합니다. - 사람이 요청했고 SA가 부수적으로 연결된 경우(예: 사람이 SA를 리뷰어로 지정): 내부적으로
:permission_check컨텍스트로 표시됩니다. 사람을 반환합니다. 이때 사람이 액터이며 사람에게 귀속되어야 합니다. - 복합 ID가 없는 경우:
current_user를 그대로 반환합니다.
상황에 따라 이 메서드를 다르게 호출할 필요는 없습니다. ID 시스템이 요청 인증 시점에 컨텍스트를 기록하고, resolve_composite_identity_actor가 이를 자동으로 사용합니다.
MR 작성자 및 Git 커밋 작성자 예외#
머지 리퀘스트의 작성자를 설정할 때는 resolve_composite_identity_actor를 사용하지 않습니다.
MR 작성자는 요청이 :authentication 컨텍스트(서비스 계정 토큰)에서 이루어진 경우에도
항상 사람 사용자(복합 ID 해석 이전의 current_user)여야 합니다.
이 예외는 컴플라이언스와 직무 분리를 위해 존재합니다.
GitLab은 사용자가 자신의 MR을 승인할 수 없도록 강제합니다(merge_requests_author_approval의 기본값은 false).
서비스 계정이 MR 작성자로 설정되면 사람 사용자가 해당 MR을 직접 승인할 수 있어 자기 승인 검사를 우회하게 됩니다.
사람을 MR 작성자로 유지하면 자기 승인 방지 장치가 계속 유효합니다.
이것이 사람이 시작한 트리거에 대한 핵심 컴플라이언스 보장입니다.
실행기가 Git CLI로 직접 푸시하는 커밋의 경우 Git은 author와 committer를 따로 기록하며,
둘은 서로 다른 액터가 됩니다.
- 커밋의
committer는 사람 사용자여야 합니다. 프로젝트에서merge_requests_disable_committers_approval이 활성화되면 GitLab은 Git 커미터 이메일로 승인자를 걸러 내므로, 사람을 커미터로 유지하면 그 사람이 승인자 풀에서 제외되어 직무 분리가 유지됩니다. 이 설정은 선택적으로 켜는 방식이며 데이터베이스 기본값이 없습니다. :authentication컨텍스트에서는resolve_composite_identity_actor가 서비스 계정을 반환하므로 Gitauthor가 서비스 계정이 됩니다. 이는 에이전트가 변경을 생성했음을 반영합니다.
이 커미터 규칙은 실행기가 Git CLI로 직접 푸시하는 커밋에만 적용됩니다. GitLab API 나 UI로 이루어진 커밋에서는 Gitaly가 서명 시 커미터를 인스턴스 ID로 대체할 수 있으며, 이 경로에서는 승인자 필터가 Git 작성자 이메일로 대체됩니다.
자율 트리거(사람이 시작하지 않은 요청)에서는 서비스 계정이 author 이자
committer 입니다. 사람을 커미터로 두는 규칙은 사람이 시작한 트리거에만 적용됩니다.
귀속 모델에 대한 사용자 관점 설명은 귀속 모델 이해하기를 참고합니다.
감사 이벤트도 동일한 귀속 규칙을 따릅니다(GitLab 19.3 이상).
:authentication컨텍스트에서는author_id가 서비스 계정을 가리키고,author_name은<service account name> on behalf of @<human username>형식으로 255자까지 잘려 저장됩니다. 사람 사용자는 이벤트details에human_author_id,human_author_name,human_author_username으로 기록됩니다. 이 키들은 스트리밍 대상과 감사 이벤트 REST API로 전달됩니다.:permission_check컨텍스트에서는 사람 사용자가 감사 이벤트 작성자로 유지되며human_author_*키는 추가되지 않습니다.
참고: MR !204010, MR !223788, MR !240418
빠른 설정 확인#
# Expect 200 only if BOTH the human and service account can read the project
curl --silent --show-error --fail --header "Authorization: Bearer " \
"https://gitlab.example.com/api/v4/projects/%2F"
일반적인 결과는 다음과 같습니다.
- 403: 두 주체 중 하나에 권한이 없습니다.
- 404: 두 주체 모두에게 리소스가 보이지 않거나 리소스를 찾을 수 없습니다.
로컬/GDK 테스트 팁#
# Rails console
service_account = User.find_by_username("service_account")
service_account.update!(composite_identity_enforced: true)
문제 해결#
- 동적 범위가 거부되거나 무시되는 경우: OAuth 앱에
dynamic_scopes: "user:*"가 설정되어 있는지 확인합니다. - 토큰에
user:$ID가 없는 경우: 범위에 구체적인user:$ID를 포함해 권한 부여와 토큰을 다시 발급합니다. - 플랫폼 작업이 서비스 계정이 아니라 사람에게 귀속되거나 그 반대인 경우: ID가 어떤 컨텍스트로 연결되었는지 확인합니다. OAuth 및 CI 플로는
:authentication을 사용하고(SA에 귀속), 웹 및 지정 플로는:permission_check를 사용합니다(사람에게 귀속). 올바른 액터에 작업 귀속하기를 참고합니다. - 토큰 교환 시 422: 리디렉션 URI가 일치하지 않거나 권한 부여가 만료되었습니다.
- API 요청 시 403: 두 주체 모두 필요한 프로젝트 또는 그룹 권한을 가지고 있는지, 그리고 기본 범위(예:
api)가 포함되어 있는지 확인합니다.