From f13decdf0c70ef3216014e7c05ae89ae4779688b Mon Sep 17 00:00:00 2001 From: Poonam Dharamkar Date: Mon, 10 Aug 2026 13:30:45 -0400 Subject: [PATCH] feat: extend org admin authority to team management - isTeamAdminOrOrgOwner now recognizes org admins (not just owners) via org_members - Add legacy org document admins/owners fallback in resolveOrgRoleForTeam - teams.byUser publication includes all org teams for owner/admin members - teams.list method includes org teams and join requests for elevated members - buildTeamAbility grants team-admin capability to org-elevated users --- meteor-backend/server/org-helpers.js | 14 ++++++---- meteor-backend/server/permissions.js | 7 ++++- meteor-backend/server/teams.js | 42 ++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/meteor-backend/server/org-helpers.js b/meteor-backend/server/org-helpers.js index b94f65bd..579e915b 100644 --- a/meteor-backend/server/org-helpers.js +++ b/meteor-backend/server/org-helpers.js @@ -170,15 +170,19 @@ export async function getAccessibleOrgIds(userId) { /** * True if `userId` has team-admin authority on `team` — either because they - * are listed in `team.admins`, or because they own the organization the team - * belongs to. Org owners get full team-admin authority on every team in - * their org (rename, delete, invite, remove member, set role/password, - * approve/decline join requests, manage invitations). + * are listed in `team.admins`, or because they are an owner/admin of the + * organization the team belongs to. Org owners and admins get full team-admin + * authority on every team in their org (rename, delete, invite, remove member, + * set role/password, approve/decline join requests, manage invitations). */ export async function isTeamAdminOrOrgOwner(team, userId) { if (team.admins.includes(userId)) return true; if (!team.orgId || !isValidId(team.orgId)) return false; + // Check modern org_members collection first (owner or admin both qualify) + const membership = await rawDb().collection('org_members').findOne({ orgId: team.orgId, userId }); + if (membership?.role === 'owner' || membership?.role === 'admin') return true; + // Fallback to legacy owners/admins arrays on the org document const org = await rawDb().collection('organizations').findOne({ _id: new ObjectId(team.orgId) }); - return !!org?.owners?.includes(userId); + return !!org?.owners?.includes(userId) || !!org?.admins?.includes(userId); } diff --git a/meteor-backend/server/permissions.js b/meteor-backend/server/permissions.js index 5c292d29..33f97777 100644 --- a/meteor-backend/server/permissions.js +++ b/meteor-backend/server/permissions.js @@ -68,6 +68,10 @@ async function resolveOrgRoleForTeam(userId, team) { const membership = await rawDb().collection('org_members').findOne({ orgId: team.orgId, userId }); if (membership?.role === 'owner') return 'owner'; if (membership?.role === 'admin') return 'admin'; + // Fallback to legacy owners/admins arrays for data predating org_members migration + const org = await rawDb().collection('organizations').findOne({ _id: new ObjectId(team.orgId) }); + if ((org?.owners ?? []).includes(userId)) return 'owner'; + if ((org?.admins ?? []).includes(userId)) return 'admin'; return 'member'; } @@ -118,7 +122,8 @@ export async function buildTeamAbility(userId, teamId) { orgIds: team.orgId ? [team.orgId] : [], enterpriseIds: enterpriseScope.enterpriseId ? [enterpriseScope.enterpriseId] : [], isEnterpriseElevated: enterpriseScope.elevated, - teamAdminIds: (team.admins ?? []).includes(userId) ? [teamId] : [], + // Org owners/admins have team-admin authority on all teams in their org + teamAdminIds: (team.admins ?? []).includes(userId) || isOrgElevated ? [teamId] : [], }); return { team, scoped, ability }; diff --git a/meteor-backend/server/teams.js b/meteor-backend/server/teams.js index 102d0ffc..f0559aab 100644 --- a/meteor-backend/server/teams.js +++ b/meteor-backend/server/teams.js @@ -110,26 +110,52 @@ function toPublicTeam(team) { }; } -Meteor.publish('teams.byUser', function () { +Meteor.publish('teams.byUser', async function () { if (!this.userId) return this.ready(); const userId = this.userId; - return Teams.find({ members: userId }); + // Also surface all teams in orgs where this user is an owner or admin + const elevatedMemberships = await rawDb() + .collection('org_members') + .find({ userId, role: { $in: ['owner', 'admin'] } }) + .toArray(); + const elevatedOrgIds = elevatedMemberships.map((m) => m.orgId); + const filter = + elevatedOrgIds.length > 0 + ? { $or: [{ members: userId }, { orgId: { $in: elevatedOrgIds }, isPersonal: { $ne: true } }] } + : { members: userId }; + return Teams.find(filter); }); Meteor.methods({ async 'teams.list'() { const identity = await requireIdentity(this); - const teams = await Teams.find({ members: identity.userId }).fetchAsync(); + const userId = identity.userId; + + // Include all teams in orgs where this user is an owner or admin + const elevatedMemberships = await rawDb() + .collection('org_members') + .find({ userId, role: { $in: ['owner', 'admin'] } }) + .toArray(); + const elevatedOrgIds = elevatedMemberships.map((m) => m.orgId); + const teamFilter = + elevatedOrgIds.length > 0 + ? { $or: [{ members: userId }, { orgId: { $in: elevatedOrgIds }, isPersonal: { $ne: true } }] } + : { members: userId }; + const teams = await Teams.find(teamFilter).fetchAsync(); const userPending = await TeamJoinRequests.rawCollection() - .find({ userId: identity.userId, status: 'pending' }) + .find({ userId, status: 'pending' }) .sort({ requestedAt: -1 }) .toArray(); - const adminTeamIds = teams.filter((t) => t.admins?.includes(identity.userId)).map((t) => { - const id = t._id?.toHexString ? t._id.toHexString() : String(t._id); - return id; - }); + // Org owners/admins see pending join requests for all their teams + const adminTeamIds = teams + .filter((t) => { + const id = t._id?.toHexString ? t._id.toHexString() : String(t._id); + if (t.admins?.includes(userId)) return true; + return elevatedOrgIds.includes(t.orgId); + }) + .map((t) => (t._id?.toHexString ? t._id.toHexString() : String(t._id))); let adminPending = []; if (adminTeamIds.length > 0) {