ExchangeToken exchanges an external OIDC token for a Bytebase access token. Used by CI/CD pipelines with Workload Identity Federation.
(ctx context.Context, req *connect.Request[v1pb.ExchangeTokenRequest])
| 1375 | // ExchangeToken exchanges an external OIDC token for a Bytebase access token. |
| 1376 | // Used by CI/CD pipelines with Workload Identity Federation. |
| 1377 | func (s *AuthService) ExchangeToken(ctx context.Context, req *connect.Request[v1pb.ExchangeTokenRequest]) (*connect.Response[v1pb.ExchangeTokenResponse], error) { |
| 1378 | request := req.Msg |
| 1379 | |
| 1380 | if request.Token == "" { |
| 1381 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("token is required")) |
| 1382 | } |
| 1383 | if request.Email == "" { |
| 1384 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("email is required")) |
| 1385 | } |
| 1386 | |
| 1387 | if err := validateWorkloadIdentityEmail(request.Email); err != nil { |
| 1388 | return nil, connect.NewError(connect.CodeInvalidArgument, |
| 1389 | invalidAccountEmailError("workload identity", request.Email, err)) |
| 1390 | } |
| 1391 | |
| 1392 | // Find workload identity by email (cross-workspace lookup since this is unauthenticated). |
| 1393 | wi, err := s.store.GetWorkloadIdentityByEmail(ctx, request.Email) |
| 1394 | if err != nil { |
| 1395 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to find workload identity")) |
| 1396 | } |
| 1397 | if wi == nil { |
| 1398 | return nil, connect.NewError(connect.CodeNotFound, errors.Errorf("workload identity %q not found", request.Email)) |
| 1399 | } |
| 1400 | // Announce the workspace as soon as we know it (from the WI record) so |
| 1401 | // that a deactivated-WI attempt — which compliance wants to see — still |
| 1402 | // lands in the audit log. |
| 1403 | common.SetAuditWorkspaceID(ctx, wi.Workspace) |
| 1404 | if wi.MemberDeleted { |
| 1405 | return nil, connect.NewError(connect.CodeUnauthenticated, |
| 1406 | errors.New("workload identity has been deactivated")) |
| 1407 | } |
| 1408 | |
| 1409 | // Get workload identity config |
| 1410 | wicConfig := wi.Config |
| 1411 | if wicConfig == nil { |
| 1412 | return nil, connect.NewError(connect.CodeInternal, |
| 1413 | errors.New("workload identity config not found")) |
| 1414 | } |
| 1415 | |
| 1416 | // Validate OIDC token |
| 1417 | if _, err = wif.ValidateToken(ctx, request.Token, wicConfig); err != nil { |
| 1418 | return nil, connect.NewError(connect.CodeUnauthenticated, |
| 1419 | errors.Wrap(err, "token validation failed")) |
| 1420 | } |
| 1421 | |
| 1422 | // Generate Bytebase API token using workspace from the WI record. |
| 1423 | token, err := auth.GenerateAPIToken(wi.Email, wi.Workspace, s.secret) |
| 1424 | if err != nil { |
| 1425 | return nil, connect.NewError(connect.CodeInternal, |
| 1426 | errors.Wrap(err, "failed to generate access token")) |
| 1427 | } |
| 1428 | |
| 1429 | return connect.NewResponse(&v1pb.ExchangeTokenResponse{ |
| 1430 | AccessToken: token, |
| 1431 | }), nil |
| 1432 | } |
| 1433 | |
| 1434 | func getAccountRestriction( |
nothing calls this directly
no test coverage detected