| 206 | } |
| 207 | |
| 208 | func listUserImpl(ctx context.Context, txn *sql.Tx, find *FindUserMessage) ([]*UserMessage, error) { |
| 209 | with := qb.Q() |
| 210 | from := qb.Q().Space("principal") |
| 211 | where := qb.Q().Space("TRUE") |
| 212 | |
| 213 | // Scope users by workspace or project membership. |
| 214 | if v := find.ProjectID; v != nil { |
| 215 | // Project filter: include users who are members of the project or workspace. |
| 216 | with.Space(`WITH all_members AS ( |
| 217 | SELECT |
| 218 | jsonb_array_elements_text(jsonb_array_elements(policy.payload->'bindings')->'members') AS member, |
| 219 | jsonb_array_elements(policy.payload->'bindings')->>'role' AS role |
| 220 | FROM policy |
| 221 | WHERE ((resource_type = ? AND resource = ?) OR resource_type = ?) AND type = ? AND policy.workspace = ? |
| 222 | ), |
| 223 | project_members AS ( |
| 224 | SELECT ARRAY_AGG(member) AS members FROM all_members WHERE role NOT LIKE 'roles/workspace%' |
| 225 | )`, storepb.Policy_PROJECT.String(), "projects/"+*v, storepb.Policy_WORKSPACE.String(), storepb.Policy_IAM.String(), find.Workspace) |
| 226 | from.Space(`INNER JOIN project_members ON (CONCAT('users/', principal.email) = ANY(project_members.members) OR ? = ANY(project_members.members))`, common.AllUsers) |
| 227 | } else if find.Workspace != "" { |
| 228 | // Workspace IAM filter (no project): include only users who are members of the workspace IAM policy. |
| 229 | // Used in SaaS mode to prevent cross-workspace user listing. |
| 230 | with.Space(`WITH workspace_members AS ( |
| 231 | SELECT ARRAY_AGG(DISTINCT member) AS members |
| 232 | FROM ( |
| 233 | SELECT jsonb_array_elements_text(jsonb_array_elements(policy.payload->'bindings')->'members') AS member |
| 234 | FROM policy |
| 235 | WHERE resource_type = ? AND type = ? AND policy.workspace = ? |
| 236 | ) sub |
| 237 | )`, storepb.Policy_WORKSPACE.String(), storepb.Policy_IAM.String(), find.Workspace) |
| 238 | from.Space(`INNER JOIN workspace_members ON (CONCAT('users/', principal.email) = ANY(workspace_members.members) OR ? = ANY(workspace_members.members))`, common.AllUsers) |
| 239 | } |
| 240 | |
| 241 | if filterQ := find.FilterQ; filterQ != nil { |
| 242 | where.And("?", filterQ) |
| 243 | } |
| 244 | if v := find.ID; v != nil { |
| 245 | where.And("principal.id = ?", *v) |
| 246 | } |
| 247 | if v := find.Email; v != nil { |
| 248 | if *v == common.AllUsers { |
| 249 | where.And("principal.email = ?", *v) |
| 250 | } else { |
| 251 | where.And("principal.email = ?", strings.ToLower(*v)) |
| 252 | } |
| 253 | } |
| 254 | if !find.ShowDeleted { |
| 255 | where.And("principal.deleted = ?", false) |
| 256 | } |
| 257 | |
| 258 | q := qb.Q().Space("?", with) |
| 259 | q.Space(` |
| 260 | SELECT |
| 261 | principal.id AS user_id, |
| 262 | principal.deleted, |
| 263 | principal.email, |
| 264 | principal.name, |
| 265 | principal.password_hash, |