Declare allowExternalMembers on managed groups so deploys stop re-arming the external-member purge (#140)
* Declare allowExternalMembers on managed group settings Every pulumi up was resetting the Google Workspace per-group setting ALLOW_EXTERNAL_MEMBERS to false on all managed groups, because the GroupSettings resources never declared allowExternalMembers and the provider (SamuZad/googleworkspace 0.11.1) defaults the field to false. Google then silently purges external-email members from affected groups roughly 1-2 days after each apply, with no audit events. Pin the setting to true for all managed groups. External membership itself remains governed exclusively by config/users.ts entries; this setting only permits it. Part of the #133 incident. * Scope allowExternalMembers per group, derived from membership config Instead of permitting external members on every managed group, derive allowExternalMembers per group: true only for groups whose membership config (config/users.ts) resolves at least one member to a non-@modelcontextprotocol.io email. Email resolution is factored into resolveGoogleMemberEmail and shared with GroupMember creation so the two cannot drift. The field stays declared on every group because the provider defaults it to false on each apply, after which Google silently purges external members (#133 incident). With current config, exactly these groups derive true: maintainers, registry-wg, antitrust, catch-all. A config test pins this set so changes to it are always deliberate. * Make allowExternalMembers an explicit per-role opt-in with config validation Replace the derived per-group allowExternalMembers (computed from membership config) with an explicit opt-in: group owners set allowExternalMembers: true on the role's google config in src/config/roles.ts, and src/google.ts passes it straight through to GroupSettings (defaulting to false). The field stays declared on every group because the provider resets an omitted field to false on each apply, after which Google silently purges external members (#133). Roles opting in, matching current membership: maintainers, registry-maintainers (registry-wg), antitrust, catch-all. A validate-config check replaces the old pinned-set test: it fails with an actionable error whenever a group has a member resolving to a non-@modelcontextprotocol.io email but its role does not opt in, naming the group, the member email, and the exact config line to add. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
89cb9de125
commit
5917af2589
2 changed files with 63 additions and 9 deletions
|
|
@ -28,6 +28,13 @@ export interface GoogleConfig {
|
|||
isEmailGroup?: boolean;
|
||||
/** If true, members of this role get a Google Workspace user account */
|
||||
provisionUser?: boolean;
|
||||
/**
|
||||
* Opt-in for groups that intentionally include non-@modelcontextprotocol.io
|
||||
* members. Defaults to false. Must stay declared on the GroupSettings
|
||||
* resource: the provider resets an omitted field to false on every apply,
|
||||
* after which Google silently purges external members (#133 incident).
|
||||
*/
|
||||
allowExternalMembers?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,7 +112,7 @@ export const ROLES: readonly Role[] = [
|
|||
discord: { role: 'maintainers (synced)' },
|
||||
// GWS user accounts are opt-in: maintainers add firstName/lastName/googleEmailPrefix
|
||||
// to their entry in users.ts via PR to get an @modelcontextprotocol.io account
|
||||
google: { group: 'maintainers', provisionUser: true },
|
||||
google: { group: 'maintainers', provisionUser: true, allowExternalMembers: true },
|
||||
},
|
||||
{
|
||||
id: ROLE_IDS.DOCS_MAINTAINERS,
|
||||
|
|
@ -136,7 +143,7 @@ export const ROLES: readonly Role[] = [
|
|||
description: 'Official registry builders and maintainers',
|
||||
github: { team: 'registry-wg', parent: ROLE_IDS.WORKING_GROUPS },
|
||||
discord: { role: 'registry maintainers (synced)' },
|
||||
google: { group: 'registry-wg', provisionUser: true },
|
||||
google: { group: 'registry-wg', provisionUser: true, allowExternalMembers: true },
|
||||
},
|
||||
{
|
||||
id: ROLE_IDS.REGISTRY_COLLABORATORS,
|
||||
|
|
@ -394,7 +401,7 @@ export const ROLES: readonly Role[] = [
|
|||
{
|
||||
id: ROLE_IDS.ANTITRUST,
|
||||
description: 'Antitrust compliance contacts',
|
||||
google: { group: 'antitrust', isEmailGroup: true },
|
||||
google: { group: 'antitrust', isEmailGroup: true, allowExternalMembers: true },
|
||||
// Google only
|
||||
},
|
||||
{
|
||||
|
|
@ -406,7 +413,7 @@ export const ROLES: readonly Role[] = [
|
|||
{
|
||||
id: ROLE_IDS.CATCH_ALL,
|
||||
description: 'Catch-all email group',
|
||||
google: { group: 'catch-all', isEmailGroup: true },
|
||||
google: { group: 'catch-all', isEmailGroup: true, allowExternalMembers: true },
|
||||
// Google only
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ export interface Member {
|
|||
skipGoogleUserProvisioning?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the email a member joins Google groups with.
|
||||
* Prefers the provisioned GWS email over the personal email. This is the
|
||||
* single source of truth for group-membership emails — src/google.ts uses it
|
||||
* to create memberships, and scripts/validate-config.ts uses it to check that
|
||||
* external members only appear in roles that opt into allowExternalMembers.
|
||||
*/
|
||||
export function resolveGoogleMemberEmail(member: Member): string | undefined {
|
||||
return member.googleEmailPrefix
|
||||
? `${member.googleEmailPrefix}@modelcontextprotocol.io`
|
||||
: member.email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort roles by GitHub parent dependency (topological sort).
|
||||
* Ensures parent teams are created before child teams.
|
||||
|
|
|
|||
Loading…
Reference in a new issue