ListWorkspacesByEmail finds all workspaces where the given email is a member in the workspace IAM policy bindings, either directly or via a group. Returns workspaces sorted by name.
(ctx context.Context, find *FindWorkspaceMessage)
| 278 | // in the workspace IAM policy bindings, either directly or via a group. |
| 279 | // Returns workspaces sorted by name. |
| 280 | func (s *Store) ListWorkspacesByEmail(ctx context.Context, find *FindWorkspaceMessage) ([]*WorkspaceMessage, error) { |
| 281 | memberName := common.FormatUserEmail(find.Email) |
| 282 | |
| 283 | // Check direct membership OR group membership: |
| 284 | // 1. Direct: member = 'users/{email}' |
| 285 | // 2. Group: member = 'groups/{groupEmail}' and user_group with that email |
| 286 | // contains 'users/{email}' in its payload.members[].member |
| 287 | // 3. AllUsers: member = 'allUsers' (self-hosted only) |
| 288 | memberFilter := qb.Q().Space("member = ?", memberName) |
| 289 | memberFilter.Or(`member LIKE 'groups/%' AND EXISTS ( |
| 290 | SELECT 1 |
| 291 | FROM user_group ug, |
| 292 | jsonb_array_elements(ug.payload->'members') AS gm |
| 293 | WHERE ug.workspace = w.resource_id |
| 294 | AND ('groups/' || ug.email = member OR 'groups/' || ug.id = member) |
| 295 | AND gm->>'member' = ? |
| 296 | )`, memberName) |
| 297 | if find.IncludeAllUser { |
| 298 | memberFilter.Or("member = ?", common.AllUsers) |
| 299 | } |
| 300 | |
| 301 | q := qb.Q().Space(` |
| 302 | SELECT DISTINCT w.resource_id, w.payload, w.created_at |
| 303 | FROM workspace w |
| 304 | JOIN policy p ON p.workspace = w.resource_id |
| 305 | WHERE p.resource_type = ? |
| 306 | AND p.type = ? |
| 307 | AND w.deleted = FALSE |
| 308 | AND EXISTS ( |
| 309 | SELECT 1 |
| 310 | FROM jsonb_array_elements(p.payload->'bindings') AS binding, |
| 311 | jsonb_array_elements_text(binding->'members') AS member |
| 312 | WHERE ? |
| 313 | ) |
| 314 | `, storepb.Policy_WORKSPACE.String(), storepb.Policy_IAM.String(), memberFilter) |
| 315 | if v := find.WorkspaceID; v != nil { |
| 316 | q.And("w.resource_id = ?", *v) |
| 317 | } |
| 318 | q.Space("ORDER BY w.created_at") |
| 319 | |
| 320 | query, args, err := q.ToSQL() |
| 321 | if err != nil { |
| 322 | return nil, errors.Wrapf(err, "failed to build sql") |
| 323 | } |
| 324 | |
| 325 | rows, err := s.GetDB().QueryContext(ctx, query, args...) |
| 326 | if err != nil { |
| 327 | return nil, errors.Wrap(err, "failed to find workspaces by member") |
| 328 | } |
| 329 | defer rows.Close() |
| 330 | |
| 331 | var workspaces []*WorkspaceMessage |
| 332 | for rows.Next() { |
| 333 | var ws WorkspaceMessage |
| 334 | var payloadBytes []byte |
| 335 | var createdAt any |
| 336 | if err := rows.Scan(&ws.ResourceID, &payloadBytes, &createdAt); err != nil { |
| 337 | return nil, errors.Wrap(err, "failed to scan workspace") |