Skip to content

Group Types Overview

The PostgreSQL Permissions Model supports three distinct group types to accommodate various authentication and authorization scenarios. This flexible approach allows integration with any identity provider while maintaining internal group management capabilities.

Three Group Models

1. Internal Groups

Traditional database-managed groups where membership is explicitly stored and managed within the PostgreSQL database.

Use Cases: - Application-specific roles - Custom business groups - Manual user assignment - Groups independent of external systems

2. External Groups

Groups where membership is determined entirely by external identity provider mappings. No direct membership is stored in the database.

Use Cases: - Active Directory integration - Azure AD group synchronization - LDAP group mapping - SSO provider groups (Okta, Auth0, KeyCloak)

3. Hybrid Groups

Combines both internal and external membership models. Users can be added directly OR inherit membership through external mappings.

Use Cases: - Gradual migration from internal to external - Override external groups with exceptions - Temporary access additions - Mixed authentication environments

Database Structure

Core Group Table

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 CHECK (group_type IN ('internal', 'external', 'hybrid')),
    is_active boolean DEFAULT true,
    created_at timestamptz DEFAULT now(),
    updated_at timestamptz DEFAULT now(),
    UNIQUE (tenant_id, code)
);

Membership Tables

-- Internal/Hybrid group membership
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,
    PRIMARY KEY (user_group_id, user_id)
);

-- External group mappings
CREATE TABLE auth.user_group_mapping (
    mapping_id uuid PRIMARY KEY,
    user_group_id uuid REFERENCES auth.user_group,
    provider_id uuid REFERENCES auth.provider,
    external_group_name text,
    external_role_name text,
    is_active boolean DEFAULT true
);

How Group Types Work

Internal Groups Flow

graph LR
    A[User] --> B[Internal Group]
    B --> C[Direct Membership]
    C --> D[Permissions]
  1. Admin explicitly adds user to group
  2. Membership stored in user_group_member
  3. User inherits group permissions
  4. Membership persists until explicitly removed

External Groups Flow

graph LR
    A[User Login] --> B[Identity Provider]
    B --> C[Provider Groups/Roles]
    C --> D[Group Mapping]
    D --> E[Internal Permissions]
  1. User logs in via external provider
  2. Provider returns groups/roles in token
  3. System maps external groups to internal groups
  4. User gets permissions without stored membership

Hybrid Groups Flow

graph LR
    A[User] --> B[Hybrid Group]
    B --> C[Direct Member?]
    C -->|Yes| D[Has Access]
    C -->|No| E[Check External]
    E --> F[External Mapping?]
    F -->|Yes| D
    F -->|No| G[No Access]

Creating Different Group Types

Create Internal Group

-- Create internal group
SELECT auth.create_user_group(
    _tenant_id := 'tenant-uuid',
    _code := 'DEVELOPERS',
    _name := 'Development Team',
    _description := 'Internal development team members',
    _group_type := 'internal'
);

-- Add members directly
SELECT auth.create_user_group_member(
    _user_group_id := 'group-uuid',
    _user_id := 'user-uuid'
);

Create External Group

-- Create external group
SELECT auth.create_external_user_group(
    _tenant_id := 'tenant-uuid',
    _code := 'AD_ADMINS',
    _name := 'Active Directory Administrators',
    _description := 'Synced from AD admin group'
);

-- Map external groups
SELECT auth.create_user_group_mapping(
    _user_group_id := 'group-uuid',
    _provider_id := 'azure-ad-provider-uuid',
    _external_group_name := 'Domain Admins',
    _external_role_name := NULL
);

Create Hybrid Group

-- Create hybrid group
SELECT auth.create_user_group(
    _tenant_id := 'tenant-uuid',
    _code := 'SUPPORT_TEAM',
    _name := 'Customer Support',
    _group_type := 'hybrid'
);

-- Add direct members
SELECT auth.create_user_group_member(
    _user_group_id := 'group-uuid',
    _user_id := 'user-uuid'
);

-- Also map external groups
SELECT auth.create_user_group_mapping(
    _user_group_id := 'group-uuid',
    _provider_id := 'provider-uuid',
    _external_group_name := 'HelpDesk'
);

Converting Between Group Types

Internal to External

-- Convert internal group to external
SELECT auth.set_user_group_as_external(
    _user_group_id := 'group-uuid'
);
-- This removes all direct memberships

External to Hybrid

-- Convert external group to hybrid
SELECT auth.set_user_group_as_hybrid(
    _user_group_id := 'group-uuid'
);
-- Now can add direct members while keeping mappings

Any to Internal

-- Convert to internal only
UPDATE auth.user_group
SET group_type = 'internal'
WHERE user_group_id = 'group-uuid';

-- Remove external mappings
DELETE FROM auth.user_group_mapping
WHERE user_group_id = 'group-uuid';

Permission Resolution by Group Type

Check User's Effective Groups

-- Get all groups for a user (internal + external)
WITH user_groups AS (
    -- Internal/Hybrid direct membership
    SELECT
        ug.user_group_id,
        ug.code,
        ug.name,
        'direct' as membership_source
    FROM auth.user_group ug
    JOIN auth.user_group_member ugm ON ug.user_group_id = ugm.user_group_id
    WHERE ugm.user_id = 'user-uuid'
      AND ug.group_type IN ('internal', 'hybrid')

    UNION

    -- External/Hybrid mapped membership
    SELECT
        ug.user_group_id,
        ug.code,
        ug.name,
        'mapped' as membership_source
    FROM auth.user_group ug
    JOIN auth.user_group_mapping ugma ON ug.user_group_id = ugma.user_group_id
    JOIN auth.user_identity ui ON ugma.provider_id = ui.provider_id
    WHERE ui.user_id = 'user-uuid'
      AND ui.is_last_used = true
      AND ug.group_type IN ('external', 'hybrid')
      AND (
          ugma.external_group_name = ANY(ui.provider_groups) OR
          ugma.external_role_name = ANY(ui.provider_roles)
      )
)
SELECT * FROM user_groups;

Choosing the Right Group Type

Decision Matrix

Scenario Recommended Type Reason
Pure database application Internal Full control, no external dependencies
Enterprise with AD/LDAP External Leverage existing infrastructure
SaaS with multiple auth methods Hybrid Flexibility for different customers
Migration from legacy system Hybrid Gradual transition capability
API-only services Internal Simple programmatic management
SSO-only authentication External No duplicate user management

Best Practices by Type

Internal Groups: - Document group purposes clearly - Implement approval workflows - Regular membership audits - Clear naming conventions

External Groups: - Map at appropriate granularity - Handle provider outages gracefully - Cache mappings for performance - Regular sync validation

Hybrid Groups: - Clear documentation of membership sources - Precedence rules for conflicts - Audit both membership types - Monitor for orphaned members

Performance Considerations

Internal Groups

  • Direct database lookups
  • Best performance for permission checks
  • No external dependencies

External Groups

  • Requires provider group resolution
  • May need caching strategy
  • Network latency considerations

Hybrid Groups

  • Most complex resolution logic
  • Balance flexibility vs performance
  • Consider materialized views for heavy usage

Migration Strategies

Moving from Internal to External

-- Prepare migration
CREATE TEMP TABLE migration_map AS
SELECT
    ugm.user_group_id,
    ugm.user_id,
    ui.provider_groups,
    ui.provider_roles
FROM auth.user_group_member ugm
JOIN auth.user_identity ui ON ugm.user_id = ui.user_id
WHERE ui.is_last_used = true;

-- Create mappings based on analysis
INSERT INTO auth.user_group_mapping
    (user_group_id, provider_id, external_group_name)
SELECT DISTINCT
    user_group_id,
    'provider-uuid',
    unnest(provider_groups)
FROM migration_map;

-- Convert groups to external
UPDATE auth.user_group
SET group_type = 'external'
WHERE user_group_id IN (SELECT DISTINCT user_group_id FROM migration_map);

Common Patterns

Department Groups (Internal)

CREATE_INTERNAL_GROUP('FINANCE', 'Finance Department');
CREATE_INTERNAL_GROUP('MARKETING', 'Marketing Department');

Provider Groups (External)

CREATE_EXTERNAL_GROUP('AZURE_USERS', 'Azure AD Users');
MAP_EXTERNAL_GROUP('AZURE_USERS', 'azure-ad', 'Domain Users');

Override Groups (Hybrid)

CREATE_HYBRID_GROUP('SPECIAL_ACCESS', 'Special Access Users');
-- Mostly external, but can add exceptions

What's Next