Hybrid Groups¶
Hybrid Groups combine the flexibility of internal group membership management with the dynamic capabilities of external provider-based membership. This model allows groups to have both explicitly managed members (internal) and automatically resolved members from external providers (external), providing the best of both worlds for complex organizational structures.
Overview¶
Hybrid Groups use both direct database storage and external provider mappings for comprehensive membership management:
graph TB
subgraph "Hybrid Group Model"
HG[Hybrid Group<br/>auth.user_group<br/>group_type = 'hybrid']
subgraph "Internal Membership"
GM[Group Members<br/>auth.user_group_member<br/>Direct membership storage]
U1[User 1<br/>Direct Member]
U2[User 2<br/>Direct Member]
end
subgraph "External Membership"
MAP[Group Mappings<br/>auth.user_group_mapping<br/>Provider group/role mappings]
U3[User 3<br/>Via Azure AD Groups]
U4[User 4<br/>Via LDAP Groups]
U5[User 5<br/>Via SAML Roles]
end
end
HG --> GM
HG --> MAP
GM --> U1
GM --> U2
MAP --> U3
MAP --> U4
MAP --> U5
Key Characteristics: - Combines internal and external membership models - Supports both explicit and automatic membership - Flexible membership rules and override capabilities - Perfect for transitional or complex scenarios - Allows granular control over membership sources
Database Structure¶
Core Tables Integration¶
-- Hybrid group definition (same as internal/external)
CREATE TABLE auth.user_group (
user_group_id uuid PRIMARY KEY,
tenant_id uuid REFERENCES auth.tenant,
code text NOT NULL,
name text NOT NULL,
description text,
group_type text DEFAULT 'hybrid', -- Specifies hybrid behavior
is_active boolean DEFAULT true,
created_at timestamptz DEFAULT now(),
UNIQUE (tenant_id, code)
);
-- Internal membership storage (same as internal groups)
CREATE TABLE auth.user_group_member (
user_group_id uuid REFERENCES auth.user_group,
user_id uuid REFERENCES auth.user_info,
added_at timestamptz DEFAULT now(),
added_by uuid REFERENCES auth.user_info,
is_active boolean DEFAULT true,
membership_source text DEFAULT 'internal', -- Track source
PRIMARY KEY (user_group_id, user_id)
);
-- External mappings (same as external groups)
CREATE TABLE auth.user_group_mapping (
user_group_mapping_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_group_id uuid REFERENCES auth.user_group,
provider_id uuid REFERENCES auth.provider,
provider_group_pattern text,
provider_role_pattern text,
mapping_priority integer DEFAULT 100,
is_active boolean DEFAULT true,
created_at timestamptz DEFAULT now()
);
Creating Hybrid Groups¶
Basic Hybrid Group Creation¶
-- Create hybrid group
SELECT auth.create_user_group(
_tenant_id := 'tenant-uuid',
_code := 'PROJECT_ALPHA',
_name := 'Project Alpha Team',
_description := 'Project Alpha development team with mixed membership',
_group_type := 'hybrid'
);
-- Returns: user_group_id (uuid)
-- Add external mappings (for automatic membership)
SELECT auth.create_user_group_mapping(
_user_group_id := 'project-alpha-group-uuid',
_provider_id := 'azure-ad-provider-uuid',
_provider_group_pattern := 'Project-Alpha-*',
_provider_role_pattern := 'Developer',
_mapping_priority := 100
);
-- Add direct members (for explicit membership)
SELECT auth.create_user_group_member(
_user_group_id := 'project-alpha-group-uuid',
_user_id := 'external-consultant-uuid',
_added_by := 'project-manager-uuid'
);
Complex Hybrid Group Examples¶
-- Leadership group: Mix of explicit appointments and role-based
DO $$
DECLARE
v_tenant_id uuid := 'tenant-uuid';
v_leadership_group_id uuid;
BEGIN
-- Create hybrid leadership group
v_leadership_group_id := auth.create_user_group(
v_tenant_id,
'LEADERSHIP',
'Leadership Team',
'Company leadership including executives and department heads',
'hybrid'
);
-- Map external executive roles (automatic)
PERFORM auth.create_user_group_mapping(
v_leadership_group_id,
'azure-ad-provider-uuid',
NULL, -- No group pattern
'Executive', -- Role pattern
50 -- High priority
);
-- Map department head groups (automatic)
PERFORM auth.create_user_group_mapping(
v_leadership_group_id,
'azure-ad-provider-uuid',
'Department-Head-*', -- Group pattern
NULL, -- No role pattern
75 -- Medium-high priority
);
-- Add specific individuals (explicit)
-- Board members, advisors, interim leaders
INSERT INTO auth.user_group_member (user_group_id, user_id, added_by, membership_source)
VALUES
(v_leadership_group_id, 'board-member-1-uuid', 'ceo-uuid', 'board_appointment'),
(v_leadership_group_id, 'advisor-1-uuid', 'ceo-uuid', 'advisory_appointment'),
(v_leadership_group_id, 'interim-cto-uuid', 'ceo-uuid', 'interim_appointment');
-- Assign leadership permissions
PERFORM auth.assign_permission(
v_tenant_id, NULL, v_leadership_group_id, 'LEADERSHIP_PERMISSIONS', NULL
);
END $$;
Membership Resolution Logic¶
Hybrid Membership Query¶
Hybrid groups resolve membership from both sources:
-- Get all members of hybrid group (internal + external)
WITH hybrid_members AS (
-- Internal members (explicitly added)
SELECT DISTINCT
ugm.user_group_id,
ugm.user_id,
'internal' as membership_type,
ugm.added_at as membership_date,
ugm.membership_source
FROM auth.user_group_member ugm
WHERE ugm.user_group_id = 'hybrid-group-uuid'
AND ugm.is_active = true
UNION
-- External members (via provider mappings)
SELECT DISTINCT
ugmp.user_group_id,
ui.user_id,
'external' as membership_type,
ui.last_login_at as membership_date,
'provider_mapping' as membership_source
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
JOIN auth.user_info u ON ui.user_id = u.user_id
WHERE ugmp.user_group_id = 'hybrid-group-uuid'
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
AND u.is_active = true
AND (
(ugmp.provider_group_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_groups) AS pg(group_name)
WHERE pg.group_name ~ ugmp.provider_group_pattern
))
OR
(ugmp.provider_role_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_roles) AS pr(role_name)
WHERE pr.role_name ~ ugmp.provider_role_pattern
))
)
)
SELECT
hm.user_group_id,
hm.user_id,
u.username,
u.display_name,
hm.membership_type,
hm.membership_date,
hm.membership_source
FROM hybrid_members hm
JOIN auth.user_info u ON hm.user_id = u.user_id
ORDER BY hm.membership_type, u.display_name;
Membership Priority and Conflicts¶
-- Handle membership conflicts and priorities
CREATE OR REPLACE FUNCTION auth.resolve_hybrid_group_membership(
_user_group_id uuid,
_user_id uuid
) RETURNS TABLE (
is_member boolean,
membership_type text,
membership_source text,
effective_date timestamptz
) AS $$
DECLARE
v_internal_member boolean := false;
v_external_member boolean := false;
v_internal_date timestamptz;
v_external_date timestamptz;
BEGIN
-- Check internal membership
SELECT true, ugm.added_at INTO v_internal_member, v_internal_date
FROM auth.user_group_member ugm
WHERE ugm.user_group_id = _user_group_id
AND ugm.user_id = _user_id
AND ugm.is_active = true;
-- Check external membership
SELECT true, ui.last_login_at INTO v_external_member, v_external_date
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
WHERE ugmp.user_group_id = _user_group_id
AND ui.user_id = _user_id
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
AND (
(ugmp.provider_group_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_groups) AS pg(group_name)
WHERE pg.group_name ~ ugmp.provider_group_pattern
))
OR
(ugmp.provider_role_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_roles) AS pr(role_name)
WHERE pr.role_name ~ ugmp.provider_role_pattern
))
);
-- Return membership result with priority (internal takes precedence)
IF v_internal_member THEN
RETURN QUERY SELECT true, 'internal'::text, 'explicit'::text, v_internal_date;
ELSIF v_external_member THEN
RETURN QUERY SELECT true, 'external'::text, 'provider_mapping'::text, v_external_date;
ELSE
RETURN QUERY SELECT false, 'none'::text, 'none'::text, NULL::timestamptz;
END IF;
END;
$$ LANGUAGE plpgsql;
Managing Hybrid Group Membership¶
Adding Internal Members¶
-- Add explicit members to hybrid group
SELECT auth.create_user_group_member(
_user_group_id := 'hybrid-group-uuid',
_user_id := 'contractor-uuid',
_added_by := 'manager-uuid'
);
-- Add multiple internal members with source tracking
DO $$
DECLARE
v_group_id uuid := 'project-team-hybrid-uuid';
v_admin_id uuid := 'project-manager-uuid';
v_contractors uuid[] := ARRAY[
'contractor-1-uuid', 'contractor-2-uuid', 'freelancer-uuid'
];
v_user_id uuid;
BEGIN
FOREACH v_user_id IN ARRAY v_contractors
LOOP
INSERT INTO auth.user_group_member
(user_group_id, user_id, added_by, membership_source)
VALUES
(v_group_id, v_user_id, v_admin_id, 'contractor_assignment')
ON CONFLICT (user_group_id, user_id) DO NOTHING;
END LOOP;
END $$;
Managing External Mappings¶
-- Add additional external mappings to hybrid group
SELECT auth.create_user_group_mapping(
_user_group_id := 'hybrid-group-uuid',
_provider_id := 'ldap-provider-uuid',
_provider_group_pattern := 'cn=Project-Alpha,ou=Projects,dc=company,dc=com',
_provider_role_pattern := NULL,
_mapping_priority := 80
);
-- Update mapping priority
UPDATE auth.user_group_mapping
SET mapping_priority = 90
WHERE user_group_id = 'hybrid-group-uuid'
AND provider_id = 'azure-ad-provider-uuid';
Membership Override Patterns¶
-- Override external membership with explicit control
-- Example: Temporarily exclude user from external group
INSERT INTO auth.user_group_member (user_group_id, user_id, added_by, is_active, membership_source)
VALUES (
'hybrid-group-uuid',
'user-to-exclude-uuid',
'admin-uuid',
false, -- Explicitly inactive
'manual_override'
)
ON CONFLICT (user_group_id, user_id)
DO UPDATE SET
is_active = false,
membership_source = 'manual_override';
Common Hybrid Group Patterns¶
Transition from Internal to External¶
-- Gradual transition: Start with internal, add external mappings
DO $$
DECLARE
v_group_id uuid := 'developers-group-uuid';
v_tenant_id uuid := 'tenant-uuid';
BEGIN
-- Update existing internal group to hybrid
UPDATE auth.user_group
SET group_type = 'hybrid'
WHERE user_group_id = v_group_id;
-- Add external mapping for new hires (automatic inclusion)
PERFORM auth.create_user_group_mapping(
v_group_id,
'azure-ad-provider-uuid',
'New-Developer-*',
'Junior Developer',
100
);
-- Add mapping for experienced developers
PERFORM auth.create_user_group_mapping(
v_group_id,
'azure-ad-provider-uuid',
NULL,
'Senior Developer',
80
);
-- Keep existing internal members (they remain explicit)
-- New users will be added automatically via mappings
END $$;
Project-Based Hybrid Groups¶
-- Project team with core team (internal) + department members (external)
CREATE OR REPLACE FUNCTION create_project_hybrid_group(
_tenant_id uuid,
_project_code text,
_project_name text,
_core_team_members uuid[],
_department_mapping text
) RETURNS uuid AS $$
DECLARE
v_group_id uuid;
v_user_id uuid;
BEGIN
-- Create hybrid project group
v_group_id := auth.create_user_group(
_tenant_id,
'PROJECT_' || upper(_project_code),
'Project: ' || _project_name,
_project_name || ' team with core and department members',
'hybrid'
);
-- Add core team members (internal)
FOREACH v_user_id IN ARRAY _core_team_members
LOOP
PERFORM auth.create_user_group_member(
v_group_id, v_user_id, v_user_id -- Self-added initially
);
UPDATE auth.user_group_member
SET membership_source = 'core_team'
WHERE user_group_id = v_group_id AND user_id = v_user_id;
END LOOP;
-- Add department mapping (external)
PERFORM auth.create_user_group_mapping(
v_group_id,
'azure-ad-provider-uuid',
_department_mapping, -- e.g., 'Engineering-*'
NULL,
100
);
-- Assign project permissions
PERFORM auth.assign_permission(
_tenant_id, NULL, v_group_id, 'PROJECT_MEMBER', NULL
);
RETURN v_group_id;
END;
$$ LANGUAGE plpgsql;
-- Usage
SELECT create_project_hybrid_group(
'tenant-uuid',
'BETA',
'Project Beta',
ARRAY['team-lead-uuid', 'architect-uuid', 'pm-uuid'],
'Engineering-Beta'
);
Department with Contractors¶
-- Department group: Employees via AD + contractors via explicit membership
DO $$
DECLARE
v_tenant_id uuid := 'tenant-uuid';
v_dept_group_id uuid;
BEGIN
-- Create hybrid department group
v_dept_group_id := auth.create_user_group(
v_tenant_id,
'MARKETING_EXTENDED',
'Marketing Department (Extended)',
'Marketing employees and contractors',
'hybrid'
);
-- Map full-time employees (external)
PERFORM auth.create_user_group_mapping(
v_dept_group_id,
'azure-ad-provider-uuid',
'Marketing-*',
NULL,
50 -- High priority for employees
);
-- Map marketing roles (external)
PERFORM auth.create_user_group_mapping(
v_dept_group_id,
'azure-ad-provider-uuid',
NULL,
'Marketing Specialist',
60
);
-- Contractors will be added explicitly as internal members
-- This allows for contractors who don't have AD accounts
-- or need specific access that doesn't match AD groups
-- Assign base marketing permissions
PERFORM auth.assign_permission(
v_tenant_id, NULL, v_dept_group_id, 'MARKETING_BASE', NULL
);
END $$;
Querying Hybrid Groups¶
Membership Analysis¶
-- Analyze membership composition of hybrid group
WITH membership_analysis AS (
SELECT
'Internal' as source_type,
COUNT(*) as member_count,
array_agg(DISTINCT ugm.membership_source) as sources
FROM auth.user_group_member ugm
JOIN auth.user_info u ON ugm.user_id = u.user_id
WHERE ugm.user_group_id = 'hybrid-group-uuid'
AND ugm.is_active = true
AND u.is_active = true
UNION ALL
SELECT
'External' as source_type,
COUNT(DISTINCT ui.user_id) as member_count,
array_agg(DISTINCT p.name) as sources
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
JOIN auth.provider p ON ugmp.provider_id = p.provider_id
JOIN auth.user_info u ON ui.user_id = u.user_id
WHERE ugmp.user_group_id = 'hybrid-group-uuid'
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
AND u.is_active = true
AND (
(ugmp.provider_group_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_groups) AS pg(group_name)
WHERE pg.group_name ~ ugmp.provider_group_pattern
))
OR
(ugmp.provider_role_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_roles) AS pr(role_name)
WHERE pr.role_name ~ ugmp.provider_role_pattern
))
)
)
SELECT
source_type,
member_count,
sources
FROM membership_analysis
ORDER BY source_type;
Membership Source Tracking¶
-- Track how users gained membership in hybrid group
SELECT
u.username,
u.display_name,
CASE
WHEN ugm.user_id IS NOT NULL THEN 'Internal'
WHEN ext.user_id IS NOT NULL THEN 'External'
ELSE 'Unknown'
END as membership_type,
COALESCE(ugm.membership_source, 'provider_mapping') as source_detail,
COALESCE(ugm.added_at, ext.last_login_at) as effective_date,
COALESCE(adder.display_name, ext.provider_name) as added_by_source
FROM auth.user_info u
LEFT JOIN auth.user_group_member ugm ON u.user_id = ugm.user_id
AND ugm.user_group_id = 'hybrid-group-uuid' AND ugm.is_active = true
LEFT JOIN auth.user_info adder ON ugm.added_by = adder.user_id
LEFT JOIN LATERAL (
SELECT DISTINCT
ui.user_id,
ui.last_login_at,
p.name as provider_name
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
JOIN auth.provider p ON ugmp.provider_id = p.provider_id
WHERE ugmp.user_group_id = 'hybrid-group-uuid'
AND ui.user_id = u.user_id
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
AND (
(ugmp.provider_group_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_groups) AS pg(group_name)
WHERE pg.group_name ~ ugmp.provider_group_pattern
))
OR
(ugmp.provider_role_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_roles) AS pr(role_name)
WHERE pr.role_name ~ ugmp.provider_role_pattern
))
)
) ext ON true
WHERE ugm.user_id IS NOT NULL OR ext.user_id IS NOT NULL
ORDER BY membership_type, u.display_name;
Performance Considerations¶
Optimized Membership Queries¶
-- Efficient hybrid group membership check
CREATE OR REPLACE FUNCTION auth.is_hybrid_group_member(
_user_group_id uuid,
_user_id uuid
) RETURNS boolean AS $$
DECLARE
v_is_internal_member boolean := false;
v_is_external_member boolean := false;
BEGIN
-- Quick internal check first (usually faster)
SELECT true INTO v_is_internal_member
FROM auth.user_group_member ugm
WHERE ugm.user_group_id = _user_group_id
AND ugm.user_id = _user_id
AND ugm.is_active = true
LIMIT 1;
IF v_is_internal_member THEN
RETURN true;
END IF;
-- External check if not found internally
SELECT true INTO v_is_external_member
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
WHERE ugmp.user_group_id = _user_group_id
AND ui.user_id = _user_id
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
AND (
(ugmp.provider_group_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_groups) AS pg(group_name)
WHERE pg.group_name ~ ugmp.provider_group_pattern
))
OR
(ugmp.provider_role_pattern IS NOT NULL
AND EXISTS (
SELECT 1 FROM unnest(ui.provider_roles) AS pr(role_name)
WHERE pr.role_name ~ ugmp.provider_role_pattern
))
)
LIMIT 1;
RETURN COALESCE(v_is_external_member, false);
END;
$$ LANGUAGE plpgsql;
Indexing for Hybrid Groups¶
-- Specialized indexes for hybrid group performance
CREATE INDEX idx_user_group_member_hybrid_lookup
ON auth.user_group_member(user_group_id, user_id, is_active);
CREATE INDEX idx_user_group_mapping_hybrid_lookup
ON auth.user_group_mapping(user_group_id, provider_id)
WHERE is_active = true;
CREATE INDEX idx_user_identity_hybrid_resolution
ON auth.user_identity(user_id, provider_id, is_last_used, is_active);
Best Practices for Hybrid Groups¶
Design Guidelines¶
- Clear Membership Rules: Document which users come from internal vs external sources
- Priority Handling: Establish clear precedence rules (internal usually overrides external)
- Source Tracking: Always track membership source for auditing
- Regular Reconciliation: Periodically verify membership consistency
Migration Strategies¶
-- Migrate from internal to hybrid group
CREATE OR REPLACE FUNCTION migrate_to_hybrid_group(
_user_group_id uuid,
_provider_mappings jsonb -- Array of mapping configurations
) RETURNS void AS $$
DECLARE
v_mapping jsonb;
BEGIN
-- Update group type
UPDATE auth.user_group
SET group_type = 'hybrid'
WHERE user_group_id = _user_group_id;
-- Add external mappings
FOR v_mapping IN SELECT jsonb_array_elements(_provider_mappings)
LOOP
PERFORM auth.create_user_group_mapping(
_user_group_id,
(v_mapping->>'provider_id')::uuid,
v_mapping->>'group_pattern',
v_mapping->>'role_pattern',
COALESCE((v_mapping->>'priority')::integer, 100)
);
END LOOP;
-- Update existing members to track source
UPDATE auth.user_group_member
SET membership_source = 'migrated_internal'
WHERE user_group_id = _user_group_id
AND membership_source IS NULL;
END;
$$ LANGUAGE plpgsql;
Security Considerations¶
- Access Control: Ensure external mappings are secure and properly validated
- Audit Trail: Log both internal and external membership changes
- Override Protection: Prevent accidental override of critical memberships
- Regular Reviews: Audit hybrid group configurations regularly
Troubleshooting Hybrid Groups¶
Membership Conflicts¶
-- Identify users with both internal and external membership
SELECT
u.username,
u.display_name,
'Both internal and external membership' as issue,
ugm.membership_source as internal_source,
ext.provider_name as external_source
FROM auth.user_info u
JOIN auth.user_group_member ugm ON u.user_id = ugm.user_id
JOIN LATERAL (
SELECT DISTINCT p.name as provider_name
FROM auth.user_group_mapping ugmp
JOIN auth.user_identity ui ON ugmp.provider_id = ui.provider_id
JOIN auth.provider p ON ugmp.provider_id = p.provider_id
WHERE ugmp.user_group_id = ugm.user_group_id
AND ui.user_id = u.user_id
AND ugmp.is_active = true
AND ui.is_last_used = true
AND ui.is_active = true
) ext ON true
WHERE ugm.user_group_id = 'hybrid-group-uuid'
AND ugm.is_active = true;
Performance Issues¶
-- Find hybrid groups with performance issues
SELECT
ug.code,
ug.name,
COUNT(DISTINCT ugm.user_id) as internal_members,
COUNT(DISTINCT ugmp.user_group_mapping_id) as external_mappings,
CASE
WHEN COUNT(DISTINCT ugmp.user_group_mapping_id) > 10 THEN 'Too many mappings'
WHEN COUNT(DISTINCT ugm.user_id) > 1000 THEN 'Large internal membership'
ELSE 'OK'
END as performance_status
FROM auth.user_group ug
LEFT JOIN auth.user_group_member ugm ON ug.user_group_id = ugm.user_group_id
AND ugm.is_active = true
LEFT JOIN auth.user_group_mapping ugmp ON ug.user_group_id = ugmp.user_group_id
AND ugmp.is_active = true
WHERE ug.group_type = 'hybrid'
AND ug.is_active = true
GROUP BY ug.user_group_id
ORDER BY internal_members DESC, external_mappings DESC;
What's Next¶
- Explore Group Mappings for detailed provider integration patterns
- Review Internal Groups for direct membership management
- Learn about External Groups for provider-only membership
- Understand Permission Assignment to groups