(ctx context.Context, req *connect.Request[v1pb.SetIamPolicyRequest])
| 369 | } |
| 370 | |
| 371 | func (s *WorkspaceService) SetIamPolicy(ctx context.Context, req *connect.Request[v1pb.SetIamPolicyRequest]) (*connect.Response[v1pb.IamPolicy], error) { |
| 372 | request := req.Msg |
| 373 | |
| 374 | workspaceID := common.GetWorkspaceIDFromContext(ctx) |
| 375 | policyMessage, err := s.store.GetWorkspaceIamPolicy(ctx, workspaceID) |
| 376 | if err != nil { |
| 377 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to find workspace iam policy")) |
| 378 | } |
| 379 | if request.Etag != "" && request.Etag != policyMessage.Etag { |
| 380 | return nil, connect.NewError(connect.CodeAborted, errors.New("there is concurrent update to the workspace iam policy, please refresh and try again")) |
| 381 | } |
| 382 | |
| 383 | if err := validateIAMPolicy(ctx, s.store, !s.profile.SaaS, request, policyMessage); err != nil { |
| 384 | return nil, err |
| 385 | } |
| 386 | |
| 387 | iamPolicy, err := convertToStoreIamPolicy(request.Policy) |
| 388 | if err != nil { |
| 389 | return nil, err |
| 390 | } |
| 391 | |
| 392 | users := utils.GetUsersByRoleInIAMPolicy( |
| 393 | ctx, |
| 394 | s.store, |
| 395 | common.GetWorkspaceIDFromContext(ctx), |
| 396 | store.WorkspaceAdminRole, |
| 397 | !s.profile.SaaS, |
| 398 | iamPolicy, |
| 399 | ) |
| 400 | if !containsActiveEndUser(users) { |
| 401 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("workspace must have at least one admin")) |
| 402 | } |
| 403 | |
| 404 | // Guard: count user seats in the new policy BEFORE saving. Only reject when this |
| 405 | // change increases the user seat count beyond the limit. Over-limit workspaces (e.g. |
| 406 | // after a license downgrade) may still hold steady or reduce seats, and seat-neutral |
| 407 | // edits such as adding a service account or workload identity are always allowed |
| 408 | // because those members do not occupy a seat. |
| 409 | userLimit := s.licenseService.GetUserLimit(ctx, workspaceID) |
| 410 | newCount, err := countUsersInIamPolicy(ctx, s.store, workspaceID, iamPolicy, s.profile.SaaS) |
| 411 | if err != nil { |
| 412 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to count users in IAM policy")) |
| 413 | } |
| 414 | if newCount > userLimit { |
| 415 | oldCount, err := countUsersInIamPolicy(ctx, s.store, workspaceID, policyMessage.Policy, s.profile.SaaS) |
| 416 | if err != nil { |
| 417 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to count users in current IAM policy")) |
| 418 | } |
| 419 | if newCount > oldCount { |
| 420 | return nil, connect.NewError(connect.CodeResourceExhausted, errors.Errorf("workspace has %d users, exceeding the limit of %d", newCount, userLimit)) |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | payloadBytes, err := protojson.Marshal(iamPolicy) |
| 425 | if err != nil { |
| 426 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to marshal iam policy")) |
| 427 | } |
| 428 | patch := &store.UpdatePolicyMessage{ |
nothing calls this directly
no test coverage detected