LeaveWorkspace removes the calling user from a workspace's IAM bindings, then switches to the next available workspace.
(ctx context.Context, req *connect.Request[v1pb.LeaveWorkspaceRequest])
| 266 | // LeaveWorkspace removes the calling user from a workspace's IAM bindings, |
| 267 | // then switches to the next available workspace. |
| 268 | func (s *WorkspaceService) LeaveWorkspace(ctx context.Context, req *connect.Request[v1pb.LeaveWorkspaceRequest]) (*connect.Response[v1pb.LoginResponse], error) { |
| 269 | user, ok := GetUserFromContext(ctx) |
| 270 | if !ok || user == nil { |
| 271 | return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not found")) |
| 272 | } |
| 273 | |
| 274 | workspaceID, err := common.GetWorkspaceID(req.Msg.Name) |
| 275 | if err != nil { |
| 276 | return nil, connect.NewError(connect.CodeInvalidArgument, err) |
| 277 | } |
| 278 | |
| 279 | // Verify the user is a member of the target workspace. |
| 280 | ws, err := s.store.FindWorkspace(ctx, &store.FindWorkspaceMessage{ |
| 281 | WorkspaceID: &workspaceID, |
| 282 | Email: user.Email, |
| 283 | }) |
| 284 | if err != nil { |
| 285 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to verify workspace membership")) |
| 286 | } |
| 287 | if ws == nil { |
| 288 | return nil, connect.NewError(connect.CodeNotFound, errors.Errorf("not a member of workspace %q", workspaceID)) |
| 289 | } |
| 290 | |
| 291 | memberIdentifier := common.FormatUserEmail(user.Email) |
| 292 | |
| 293 | // Simulate removing the user from all direct IAM bindings and check |
| 294 | // that at least one active admin would remain. We exclude the caller |
| 295 | // from the admin list to also account for admin-via-group bindings. |
| 296 | policyMessage, err := s.store.GetWorkspaceIamPolicy(ctx, workspaceID) |
| 297 | if err != nil { |
| 298 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to get workspace IAM policy")) |
| 299 | } |
| 300 | simulatedPolicy := &storepb.IamPolicy{} |
| 301 | for _, binding := range policyMessage.Policy.Bindings { |
| 302 | var filtered []string |
| 303 | for _, m := range binding.Members { |
| 304 | if m != memberIdentifier { |
| 305 | filtered = append(filtered, m) |
| 306 | } |
| 307 | } |
| 308 | if len(filtered) > 0 { |
| 309 | simulatedPolicy.Bindings = append(simulatedPolicy.Bindings, &storepb.Binding{ |
| 310 | Role: binding.Role, |
| 311 | Members: filtered, |
| 312 | Condition: binding.Condition, |
| 313 | }) |
| 314 | } |
| 315 | } |
| 316 | admins := utils.GetUsersByRoleInIAMPolicy(ctx, s.store, workspaceID, store.WorkspaceAdminRole, !s.profile.SaaS, simulatedPolicy) |
| 317 | // Filter out the caller — they may still appear as admin via group bindings |
| 318 | // that haven't been removed yet. |
| 319 | var otherAdmins []*store.UserMessage |
| 320 | for _, admin := range admins { |
| 321 | if admin.Email != user.Email { |
| 322 | otherAdmins = append(otherAdmins, admin) |
| 323 | } |
| 324 | } |
| 325 | if !containsActiveEndUser(otherAdmins) { |
nothing calls this directly
no test coverage detected