Skip to content

Group Mappings

Group Mappings provide the integration layer between external identity providers and the PostgreSQL Permissions Model's group system. They define how provider groups and roles translate into internal group memberships, enabling seamless integration with Active Directory, Azure AD, LDAP, SAML, and other identity systems.

Overview

Group mappings connect external identity data to internal group structures through pattern matching and rule-based assignments:

graph TB
    subgraph "Identity Providers"
        AD[Windows AD<br/>- Domain Users<br/>- Engineering-Dept<br/>- Managers]
        AAD[Azure AD<br/>- All Company<br/>- Engineering<br/>- Team-Alpha-Dev]
        LDAP[LDAP<br/>- ou=Engineering<br/>- ou=Managers<br/>- cn=Developers]
        SAML[SAML<br/>- Employee<br/>- Manager<br/>- Developer]
    end

    subgraph "Group Mappings"
        GM[auth.user_group_mapping<br/>Pattern matching rules<br/>Priority ordering<br/>Provider-specific logic]
    end

    subgraph "Internal Groups"
        IG1[EMPLOYEES<br/>External Group]
        IG2[ENGINEERING<br/>External Group]
        IG3[MANAGERS<br/>Hybrid Group]
        IG4[PROJECT_ALPHA<br/>Hybrid Group]
    end

    AD --> GM
    AAD --> GM
    LDAP --> GM
    SAML --> GM

    GM --> IG1
    GM --> IG2
    GM --> IG3
    GM --> IG4

Mapping Table Structure

Core Mapping Configuration

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,

    -- Pattern matching fields
    provider_group_pattern text,  -- Regex pattern for provider groups
    provider_role_pattern text,   -- Regex pattern for provider roles

    -- Mapping behavior
    mapping_priority integer DEFAULT 100,  -- Lower = higher priority
    is_active boolean DEFAULT true,
    is_inclusive boolean DEFAULT true,     -- true = include, false = exclude

    -- Conditions and metadata
    conditions jsonb,              -- Additional matching conditions
    mapping_metadata jsonb,        -- Extra configuration data

    created_at timestamptz DEFAULT now(),
    created_by uuid REFERENCES auth.user_info(user_id),

    CHECK (provider_group_pattern IS NOT NULL OR provider_role_pattern IS NOT NULL)
);

Essential Indexes

-- Performance indexes for mapping resolution
CREATE INDEX idx_user_group_mapping_resolution
    ON auth.user_group_mapping(provider_id, is_active, mapping_priority);

CREATE INDEX idx_user_group_mapping_group_lookup
    ON auth.user_group_mapping(user_group_id) WHERE is_active = true;

CREATE INDEX idx_user_group_mapping_patterns
    ON auth.user_group_mapping(provider_group_pattern, provider_role_pattern)
    WHERE is_active = true;

Pattern Matching Fundamentals

Group Pattern Examples

-- Exact match patterns
INSERT INTO auth.user_group_mapping (user_group_id, provider_id, provider_group_pattern)
VALUES
    -- Exact group name match
    ('employees-group-uuid', 'azure-ad-uuid', '^Domain Users$'),
    ('engineering-group-uuid', 'azure-ad-uuid', '^Engineering$'),
    ('managers-group-uuid', 'azure-ad-uuid', '^Managers$');

-- Wildcard and prefix patterns
INSERT INTO auth.user_group_mapping (user_group_id, provider_id, provider_group_pattern)
VALUES
    -- All engineering subgroups
    ('engineering-group-uuid', 'azure-ad-uuid', '^Engineering-.*'),
    -- All project groups
    ('project-members-group-uuid', 'azure-ad-uuid', '^Project-.*'),
    -- Department pattern
    ('dept-heads-group-uuid', 'azure-ad-uuid', '.*-Department-Head$');

Role Pattern Examples

-- Role-based mappings
INSERT INTO auth.user_group_mapping (user_group_id, provider_id, provider_role_pattern)
VALUES
    -- Management roles
    ('managers-group-uuid', 'azure-ad-uuid', '^(Manager|Director|VP|Executive)$'),
    -- Technical roles
    ('developers-group-uuid', 'azure-ad-uuid', '.*(Developer|Engineer|Architect).*'),
    -- Administrative roles
    ('admins-group-uuid', 'azure-ad-uuid', '.*(Admin|Administrator).*');

Combined Pattern Mappings

-- Complex mapping combining groups and roles
INSERT INTO auth.user_group_mapping
    (user_group_id, provider_id, provider_group_pattern, provider_role_pattern, mapping_priority)
VALUES
    -- Senior engineering: Must be in Engineering group AND have Senior role
    ('senior-engineering-uuid', 'azure-ad-uuid', '^Engineering$', '.*Senior.*', 50),
    -- Project leads: Any project group with Manager role
    ('project-leads-uuid', 'azure-ad-uuid', '^Project-.*', '.*Manager.*', 60),
    -- Department managers: Department group with Manager role
    ('dept-managers-uuid', 'azure-ad-uuid', '.*-Department$', 'Manager', 40);

Provider-Specific Mapping Patterns

Windows Active Directory

-- Windows AD typically uses DN-style group names
DO $$
DECLARE
    v_ad_provider_id uuid := (
        SELECT provider_id FROM auth.provider WHERE code = 'WINDOWS_AD'
    );
BEGIN
    -- Domain groups
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'EMPLOYEES'),
        v_ad_provider_id,
        '^CN=Domain Users,CN=Users,DC=company,DC=com$',  -- Full DN
        NULL,
        10  -- High priority
    );

    -- Organizational units
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ENGINEERING'),
        v_ad_provider_id,
        'CN=Engineering,OU=Departments,DC=company,DC=com',
        NULL,
        20
    );

    -- Nested group patterns
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'MANAGERS'),
        v_ad_provider_id,
        'CN=.*,OU=Management,DC=company,DC=com',
        NULL,
        30
    );
END $$;

Azure Active Directory

-- Azure AD uses display names and object IDs
DO $$
DECLARE
    v_aad_provider_id uuid := (
        SELECT provider_id FROM auth.provider WHERE code = 'AZURE_AD'
    );
BEGIN
    -- Security groups by name
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ALL_EMPLOYEES'),
        v_aad_provider_id,
        '^All Company$',
        NULL,
        10
    );

    -- Dynamic groups with patterns
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ENGINEERING'),
        v_aad_provider_id,
        '^(Engineering|Development|DevOps).*',
        NULL,
        20
    );

    -- Role-based assignments
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'GLOBAL_ADMINS'),
        v_aad_provider_id,
        NULL,
        '^Global Administrator$',
        5  -- Highest priority
    );

    -- Application roles
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'APP_USERS'),
        v_aad_provider_id,
        NULL,
        'Application\\.User',  -- Escaped dot
        50
    );
END $$;

LDAP Directory Services

-- LDAP uses DN-based group membership
DO $$
DECLARE
    v_ldap_provider_id uuid := (
        SELECT provider_id FROM auth.provider WHERE code = 'COMPANY_LDAP'
    );
BEGIN
    -- Department-based groups
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ENGINEERING'),
        v_ldap_provider_id,
        'cn=engineering,ou=departments,dc=company,dc=com',
        NULL,
        20
    );

    -- Functional groups
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'DEVELOPERS'),
        v_ldap_provider_id,
        'cn=developers,ou=roles,dc=company,dc=com',
        NULL,
        30
    );

    -- Manager group with role
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'MANAGERS'),
        v_ldap_provider_id,
        'cn=.*,ou=management,dc=company,dc=com',
        'manager',
        15
    );
END $$;

SAML Identity Providers

-- SAML uses attribute-based roles and groups
DO $$
DECLARE
    v_saml_provider_id uuid := (
        SELECT provider_id FROM auth.provider WHERE code = 'COMPANY_SAML'
    );
BEGIN
    -- Role attribute mappings
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'EMPLOYEES'),
        v_saml_provider_id,
        NULL,
        '^Employee$',
        10
    );

    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'MANAGERS'),
        v_saml_provider_id,
        NULL,
        '^Manager$',
        20
    );

    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'CONTRACTORS'),
        v_saml_provider_id,
        NULL,
        '^Contractor$',
        30
    );

    -- Group attribute mappings (if provider sends group info)
    PERFORM auth.create_user_group_mapping(
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ENGINEERING'),
        v_saml_provider_id,
        '^Engineering.*',
        NULL,
        25
    );
END $$;

Advanced Mapping Scenarios

Priority-Based Resolution

-- Complex priority mapping for overlapping patterns
CREATE TABLE mapping_priorities_example AS
SELECT
    mapping_priority,
    provider_group_pattern,
    provider_role_pattern,
    user_group_code,
    'Higher number = lower priority' as note
FROM (
    VALUES
        -- Highest priority: Specific executive roles
        (1, NULL, '^(CEO|CTO|CFO)$', 'EXECUTIVES'),

        -- High priority: Department heads
        (10, '^.*-Department$', 'Head', 'DEPT_HEADS'),

        -- Medium priority: Managers
        (20, NULL, '.*Manager.*', 'MANAGERS'),

        -- Lower priority: Department members
        (50, '^Engineering-.*', NULL, 'ENGINEERING'),
        (50, '^Marketing-.*', NULL, 'MARKETING'),
        (50, '^Sales-.*', NULL, 'SALES'),

        -- Lowest priority: All employees
        (100, 'Domain Users', NULL, 'EMPLOYEES')
) AS priority_examples(mapping_priority, provider_group_pattern, provider_role_pattern, user_group_code);

Conditional Mappings with JSON

-- Advanced mappings with JSON conditions
INSERT INTO auth.user_group_mapping
    (user_group_id, provider_id, provider_group_pattern, conditions, mapping_priority)
VALUES
    -- Map only if user has specific attribute
    ((SELECT user_group_id FROM auth.user_group WHERE code = 'SENIOR_STAFF'),
     'azure-ad-provider-uuid',
     '^Engineering$',
     '{"required_attributes": {"employeeType": "FTE", "jobLevel": "Senior"}}',
     30),

    -- Map only if user is in multiple groups
    ((SELECT user_group_id FROM auth.user_group WHERE code = 'CROSS_FUNCTIONAL'),
     'azure-ad-provider-uuid',
     NULL,
     '{"required_groups": ["Engineering", "Product"], "match_type": "all"}',
     40),

    -- Time-based mapping
    ((SELECT user_group_id FROM auth.user_group WHERE code = 'TEMP_PROJECT'),
     'azure-ad-provider-uuid',
     '^Project-Alpha$',
     '{"valid_until": "2024-12-31", "valid_from": "2024-01-01"}',
     20);

Exclusion Patterns

-- Negative mappings (exclusions)
INSERT INTO auth.user_group_mapping
    (user_group_id, provider_id, provider_group_pattern, is_inclusive, mapping_priority)
VALUES
    -- Exclude contractors from full employee group
    ((SELECT user_group_id FROM auth.user_group WHERE code = 'EMPLOYEES'),
     'azure-ad-provider-uuid',
     '^Contractors$',
     false,  -- Exclusion mapping
     5),     -- High priority exclusion

    -- Exclude service accounts from user groups
    ((SELECT user_group_id FROM auth.user_group WHERE code = 'INTERACTIVE_USERS'),
     'azure-ad-provider-uuid',
     '^Service-Accounts$',
     false,
     1);     -- Highest priority exclusion

Mapping Resolution Logic

Resolution Algorithm

-- Comprehensive mapping resolution function
CREATE OR REPLACE FUNCTION auth.resolve_user_group_mappings(
    _user_id uuid,
    _provider_id uuid DEFAULT NULL  -- NULL = use last-used identity
) RETURNS TABLE (
    user_group_id uuid,
    group_code text,
    group_name text,
    mapping_source text,
    mapping_priority integer
) AS $$
DECLARE
    v_provider_id uuid;
    v_provider_groups text[];
    v_provider_roles text[];
BEGIN
    -- Get user's provider identity (last used if not specified)
    SELECT
        COALESCE(_provider_id, ui.provider_id),
        ui.provider_groups,
        ui.provider_roles
    INTO v_provider_id, v_provider_groups, v_provider_roles
    FROM auth.user_identity ui
    WHERE ui.user_id = _user_id
      AND (_provider_id IS NULL AND ui.is_last_used = true OR ui.provider_id = _provider_id)
      AND ui.is_active = true
    LIMIT 1;

    IF v_provider_id IS NULL THEN
        RETURN; -- No matching identity found
    END IF;

    -- Return mapped groups ordered by priority
    RETURN QUERY
    WITH mapping_matches AS (
        SELECT DISTINCT
            ugm.user_group_id,
            ugm.mapping_priority,
            ugm.is_inclusive,
            CASE
                WHEN ugm.provider_group_pattern IS NOT NULL AND ugm.provider_role_pattern IS NOT NULL
                    THEN 'group_and_role'
                WHEN ugm.provider_group_pattern IS NOT NULL
                    THEN 'group_pattern'
                WHEN ugm.provider_role_pattern IS NOT NULL
                    THEN 'role_pattern'
                ELSE 'unknown'
            END as mapping_type
        FROM auth.user_group_mapping ugm
        WHERE ugm.provider_id = v_provider_id
          AND ugm.is_active = true
          AND (
              -- Group pattern match
              (ugm.provider_group_pattern IS NOT NULL
               AND EXISTS (
                   SELECT 1 FROM unnest(v_provider_groups) AS pg(group_name)
                   WHERE pg.group_name ~ ugm.provider_group_pattern
               ))
              OR
              -- Role pattern match
              (ugm.provider_role_pattern IS NOT NULL
               AND EXISTS (
                   SELECT 1 FROM unnest(v_provider_roles) AS pr(role_name)
                   WHERE pr.role_name ~ ugm.provider_role_pattern
               ))
          )
    ),
    resolved_groups AS (
        SELECT
            mm.user_group_id,
            mm.mapping_priority,
            mm.mapping_type,
            -- Apply inclusion/exclusion logic
            CASE
                WHEN NOT mm.is_inclusive THEN false
                ELSE NOT EXISTS (
                    SELECT 1 FROM mapping_matches excl
                    WHERE excl.user_group_id = mm.user_group_id
                      AND excl.is_inclusive = false
                      AND excl.mapping_priority <= mm.mapping_priority
                )
            END as include_group
        FROM mapping_matches mm
        WHERE mm.is_inclusive = true
    )
    SELECT
        rg.user_group_id,
        ug.code,
        ug.name,
        rg.mapping_type,
        rg.mapping_priority
    FROM resolved_groups rg
    JOIN auth.user_group ug ON rg.user_group_id = ug.user_group_id
    WHERE rg.include_group = true
      AND ug.is_active = true
    ORDER BY rg.mapping_priority, ug.code;
END;
$$ LANGUAGE plpgsql;

-- Usage examples
SELECT * FROM auth.resolve_user_group_mappings('user-uuid');
SELECT * FROM auth.resolve_user_group_mappings('user-uuid', 'azure-ad-provider-uuid');

Mapping Management Functions

Create Mapping with Validation

CREATE OR REPLACE FUNCTION auth.create_user_group_mapping_validated(
    _user_group_id uuid,
    _provider_id uuid,
    _provider_group_pattern text DEFAULT NULL,
    _provider_role_pattern text DEFAULT NULL,
    _mapping_priority integer DEFAULT 100,
    _conditions jsonb DEFAULT NULL
) RETURNS uuid AS $$
DECLARE
    v_mapping_id uuid;
    v_group_type text;
BEGIN
    -- Validate group type supports mappings
    SELECT group_type INTO v_group_type
    FROM auth.user_group
    WHERE user_group_id = _user_group_id;

    IF v_group_type NOT IN ('external', 'hybrid') THEN
        RAISE EXCEPTION 'Group mappings only supported for external and hybrid groups, not %', v_group_type;
    END IF;

    -- Validate pattern requirements
    IF _provider_group_pattern IS NULL AND _provider_role_pattern IS NULL THEN
        RAISE EXCEPTION 'Either provider_group_pattern or provider_role_pattern must be specified';
    END IF;

    -- Create mapping
    INSERT INTO auth.user_group_mapping
        (user_group_id, provider_id, provider_group_pattern, provider_role_pattern,
         mapping_priority, conditions)
    VALUES
        (_user_group_id, _provider_id, _provider_group_pattern, _provider_role_pattern,
         _mapping_priority, _conditions)
    RETURNING user_group_mapping_id INTO v_mapping_id;

    -- Log creation
    PERFORM auth.create_auth_event(
        NULL, -- System event
        NULL,
        '50050', -- MAPPING_CREATED
        'Group mapping created',
        jsonb_build_object(
            'mapping_id', v_mapping_id,
            'group_id', _user_group_id,
            'provider_id', _provider_id
        )
    );

    RETURN v_mapping_id;
END;
$$ LANGUAGE plpgsql;

Bulk Mapping Operations

-- Create multiple mappings from configuration
CREATE OR REPLACE FUNCTION auth.create_mappings_from_config(
    _mapping_config jsonb
) RETURNS integer AS $$
DECLARE
    v_mapping jsonb;
    v_count integer := 0;
    v_group_id uuid;
    v_provider_id uuid;
BEGIN
    FOR v_mapping IN SELECT jsonb_array_elements(_mapping_config)
    LOOP
        -- Get group and provider IDs
        SELECT user_group_id INTO v_group_id
        FROM auth.user_group
        WHERE code = v_mapping->>'group_code'
          AND tenant_id = (v_mapping->>'tenant_id')::uuid;

        SELECT provider_id INTO v_provider_id
        FROM auth.provider
        WHERE code = v_mapping->>'provider_code';

        IF v_group_id IS NOT NULL AND v_provider_id IS NOT NULL THEN
            PERFORM auth.create_user_group_mapping_validated(
                v_group_id,
                v_provider_id,
                v_mapping->>'group_pattern',
                v_mapping->>'role_pattern',
                COALESCE((v_mapping->>'priority')::integer, 100),
                v_mapping->'conditions'
            );
            v_count := v_count + 1;
        END IF;
    END LOOP;

    RETURN v_count;
END;
$$ LANGUAGE plpgsql;

-- Usage example
SELECT auth.create_mappings_from_config('[
    {
        "tenant_id": "tenant-uuid",
        "group_code": "ENGINEERING",
        "provider_code": "AZURE_AD",
        "group_pattern": "^Engineering.*",
        "priority": 50
    },
    {
        "tenant_id": "tenant-uuid",
        "group_code": "MANAGERS",
        "provider_code": "AZURE_AD",
        "role_pattern": ".*Manager.*",
        "priority": 30
    }
]'::jsonb);

Mapping Validation and Testing

Test Mapping Patterns

-- Test mapping patterns against sample data
CREATE OR REPLACE FUNCTION auth.test_group_mappings(
    _provider_id uuid,
    _test_groups text[],
    _test_roles text[]
) RETURNS TABLE (
    user_group_code text,
    user_group_name text,
    matched_by text,
    mapping_priority integer
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        ug.code,
        ug.name,
        CASE
            WHEN ugm.provider_group_pattern IS NOT NULL AND ugm.provider_role_pattern IS NOT NULL
                THEN 'Group: ' || ugm.provider_group_pattern || ' + Role: ' || ugm.provider_role_pattern
            WHEN ugm.provider_group_pattern IS NOT NULL
                THEN 'Group: ' || ugm.provider_group_pattern
            WHEN ugm.provider_role_pattern IS NOT NULL
                THEN 'Role: ' || ugm.provider_role_pattern
        END as matched_by,
        ugm.mapping_priority
    FROM auth.user_group_mapping ugm
    JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
    WHERE ugm.provider_id = _provider_id
      AND ugm.is_active = true
      AND (
          (ugm.provider_group_pattern IS NOT NULL
           AND EXISTS (
               SELECT 1 FROM unnest(_test_groups) AS tg(group_name)
               WHERE tg.group_name ~ ugm.provider_group_pattern
           ))
          OR
          (ugm.provider_role_pattern IS NOT NULL
           AND EXISTS (
               SELECT 1 FROM unnest(_test_roles) AS tr(role_name)
               WHERE tr.role_name ~ ugm.provider_role_pattern
           ))
      )
    ORDER BY ugm.mapping_priority, ug.code;
END;
$$ LANGUAGE plpgsql;

-- Test with sample data
SELECT * FROM auth.test_group_mappings(
    'azure-ad-provider-uuid',
    ARRAY['Engineering', 'Engineering-Frontend', 'Project-Alpha'],
    ARRAY['Senior Developer', 'Team Manager']
);

Mapping Coverage Analysis

-- Analyze mapping coverage across providers
WITH mapping_stats AS (
    SELECT
        p.code as provider_code,
        p.name as provider_name,
        COUNT(ugm.user_group_mapping_id) as total_mappings,
        COUNT(ugm.user_group_mapping_id) FILTER (
            WHERE ugm.provider_group_pattern IS NOT NULL
        ) as group_pattern_mappings,
        COUNT(ugm.user_group_mapping_id) FILTER (
            WHERE ugm.provider_role_pattern IS NOT NULL
        ) as role_pattern_mappings,
        COUNT(DISTINCT ugm.user_group_id) as mapped_groups
    FROM auth.provider p
    LEFT JOIN auth.user_group_mapping ugm ON p.provider_id = ugm.provider_id
        AND ugm.is_active = true
    GROUP BY p.provider_id
),
group_coverage AS (
    SELECT
        ug.group_type,
        COUNT(*) as total_groups,
        COUNT(ugm.user_group_id) as mapped_groups
    FROM auth.user_group ug
    LEFT JOIN auth.user_group_mapping ugm ON ug.user_group_id = ugm.user_group_id
        AND ugm.is_active = true
    WHERE ug.is_active = true
      AND ug.group_type IN ('external', 'hybrid')
    GROUP BY ug.group_type
)
SELECT
    'Provider Analysis' as analysis_type,
    provider_code as detail,
    total_mappings::text as value1,
    mapped_groups::text as value2
FROM mapping_stats

UNION ALL

SELECT
    'Group Coverage' as analysis_type,
    group_type as detail,
    (mapped_groups::float / total_groups * 100)::integer::text || '%' as value1,
    mapped_groups::text || '/' || total_groups::text as value2
FROM group_coverage

ORDER BY analysis_type, detail;

Best Practices for Group Mappings

Design Principles

  1. Clear Patterns: Use specific, unambiguous regex patterns
  2. Priority Management: Lower numbers = higher priority, use gaps (10, 20, 30)
  3. Documentation: Comment complex patterns and their business logic
  4. Testing: Always test patterns with sample data before deployment

Mapping Strategy Templates

-- Template: Department-based mapping
DO $$
DECLARE
    v_tenant_id uuid := 'tenant-uuid';
    v_provider_id uuid := 'provider-uuid';
    v_dept_groups text[] := ARRAY['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'];
    v_dept text;
    v_group_id uuid;
BEGIN
    FOREACH v_dept IN ARRAY v_dept_groups
    LOOP
        -- Create department group if not exists
        INSERT INTO auth.user_group (tenant_id, code, name, group_type)
        VALUES (v_tenant_id, upper(v_dept), v_dept || ' Department', 'external')
        ON CONFLICT (tenant_id, code) DO NOTHING
        RETURNING user_group_id INTO v_group_id;

        IF v_group_id IS NULL THEN
            SELECT user_group_id INTO v_group_id
            FROM auth.user_group
            WHERE tenant_id = v_tenant_id AND code = upper(v_dept);
        END IF;

        -- Create mapping
        PERFORM auth.create_user_group_mapping_validated(
            v_group_id,
            v_provider_id,
            '^' || v_dept || '.*',  -- Group pattern
            NULL,
            50  -- Standard priority
        );
    END LOOP;
END $$;

Security Considerations

-- Secure mapping validation
CREATE OR REPLACE FUNCTION auth.validate_mapping_security(
    _user_group_mapping_id uuid
) RETURNS boolean AS $$
DECLARE
    v_mapping record;
    v_issues text[] := ARRAY[]::text[];
BEGIN
    SELECT * INTO v_mapping
    FROM auth.user_group_mapping ugm
    JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
    JOIN auth.provider p ON ugm.provider_id = p.provider_id
    WHERE ugm.user_group_mapping_id = _user_group_mapping_id;

    -- Check for overly broad patterns
    IF v_mapping.provider_group_pattern ~ '^\.\*' THEN
        v_issues := v_issues || 'Pattern too broad - starts with .*';
    END IF;

    IF v_mapping.provider_role_pattern ~ '^\.\*' THEN
        v_issues := v_issues || 'Role pattern too broad - starts with .*';
    END IF;

    -- Check for dangerous regex
    IF v_mapping.provider_group_pattern ~ '.*\(\.\*\).*' THEN
        v_issues := v_issues || 'Potentially dangerous regex capturing';
    END IF;

    -- Log issues if found
    IF array_length(v_issues, 1) > 0 THEN
        PERFORM auth.create_auth_event(
            NULL, NULL, '50051', 'Mapping security validation failed',
            jsonb_build_object('mapping_id', _user_group_mapping_id, 'issues', v_issues)
        );
        RETURN false;
    END IF;

    RETURN true;
END;
$$ LANGUAGE plpgsql;

Troubleshooting Group Mappings

Common Issues and Solutions

-- Diagnostic queries for mapping issues

-- 1. Users not getting expected group membership
WITH user_mapping_debug AS (
    SELECT
        u.username,
        u.display_name,
        ui.provider_groups,
        ui.provider_roles,
        p.name as provider_name,
        ugm.provider_group_pattern,
        ugm.provider_role_pattern,
        ug.code as group_code,
        CASE
            WHEN ugm.provider_group_pattern IS NOT NULL THEN
                EXISTS (
                    SELECT 1 FROM unnest(ui.provider_groups) AS pg(name)
                    WHERE pg.name ~ ugm.provider_group_pattern
                )
            ELSE NULL
        END as group_pattern_matches,
        CASE
            WHEN ugm.provider_role_pattern IS NOT NULL THEN
                EXISTS (
                    SELECT 1 FROM unnest(ui.provider_roles) AS pr(name)
                    WHERE pr.name ~ ugm.provider_role_pattern
                )
            ELSE NULL
        END as role_pattern_matches
    FROM auth.user_info u
    JOIN auth.user_identity ui ON u.user_id = ui.user_id
    JOIN auth.provider p ON ui.provider_id = p.provider_id
    CROSS JOIN auth.user_group_mapping ugm
    JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
    WHERE u.username = 'debug-username'  -- Specify user to debug
      AND ui.is_last_used = true
      AND ui.is_active = true
      AND ugm.is_active = true
)
SELECT
    username,
    provider_name,
    group_code,
    provider_group_pattern,
    group_pattern_matches,
    provider_role_pattern,
    role_pattern_matches,
    CASE
        WHEN provider_group_pattern IS NOT NULL AND provider_role_pattern IS NOT NULL
            THEN group_pattern_matches AND role_pattern_matches
        WHEN provider_group_pattern IS NOT NULL
            THEN group_pattern_matches
        WHEN provider_role_pattern IS NOT NULL
            THEN role_pattern_matches
        ELSE false
    END as should_match
FROM user_mapping_debug
ORDER BY group_code;

-- 2. Find unused mappings
SELECT
    ug.code as group_code,
    ugm.provider_group_pattern,
    ugm.provider_role_pattern,
    COUNT(ui.user_id) as matching_users
FROM auth.user_group_mapping ugm
JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
LEFT JOIN auth.user_identity ui ON ugm.provider_id = ui.provider_id
    AND ui.is_last_used = true
    AND ui.is_active = true
    AND (
        (ugm.provider_group_pattern IS NOT NULL
         AND EXISTS (
             SELECT 1 FROM unnest(ui.provider_groups) AS pg(name)
             WHERE pg.name ~ ugm.provider_group_pattern
         ))
        OR
        (ugm.provider_role_pattern IS NOT NULL
         AND EXISTS (
             SELECT 1 FROM unnest(ui.provider_roles) AS pr(name)
             WHERE pr.name ~ ugm.provider_role_pattern
         ))
    )
WHERE ugm.is_active = true
GROUP BY ugm.user_group_mapping_id, ug.code, ugm.provider_group_pattern, ugm.provider_role_pattern
HAVING COUNT(ui.user_id) = 0
ORDER BY ug.code;

-- 3. Conflicting mappings (same priority)
SELECT
    ug1.code as group1,
    ug2.code as group2,
    ugm1.mapping_priority,
    ugm1.provider_group_pattern,
    ugm1.provider_role_pattern,
    'Same priority conflict' as issue
FROM auth.user_group_mapping ugm1
JOIN auth.user_group_mapping ugm2 ON ugm1.provider_id = ugm2.provider_id
    AND ugm1.mapping_priority = ugm2.mapping_priority
    AND ugm1.user_group_mapping_id < ugm2.user_group_mapping_id
JOIN auth.user_group ug1 ON ugm1.user_group_id = ug1.user_group_id
JOIN auth.user_group ug2 ON ugm2.user_group_id = ug2.user_group_id
WHERE ugm1.is_active = true
  AND ugm2.is_active = true
ORDER BY ugm1.mapping_priority;

What's Next