Files
wehub-resource-sync bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Waiting to run
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Blocked by required conditions
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Waiting to run
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Blocked by required conditions
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Waiting to run
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Blocked by required conditions
Java Checkstyle / java-checkstyle (push) Waiting to run
Maven Collate Tests / maven-collate-ci (push) Waiting to run
OpenMetadata Service Unit Tests / Detect Changes (push) Waiting to run
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Blocked by required conditions
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Blocked by required conditions
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Blocked by required conditions
Publish Package to Maven Central Repository / publish-maven-packages (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:35:45 +08:00

63 KiB

Bot Impersonation Feature - Design Document

1. Overview

1.1 Purpose

Enable bots in OpenMetadata to impersonate users when performing actions, ensuring proper attribution of changes to the actual user while maintaining audit trail of bot involvement.

1.2 Background

Currently, when bots perform actions (e.g., ingestion pipelines, automation workflows), the updatedBy field shows the bot's name. This obscures who actually initiated the action. With impersonation, we can track:

  • updatedBy: The actual user who initiated the action
  • impersonatedBy: The bot that executed the action on behalf of the user

1.3 Goals

  • Allow bots to act on behalf of users with proper authorization
  • Maintain complete audit trail showing both user and bot
  • Use impersonated user's permissions for authorization checks
  • Provide secure, policy-based control over impersonation capabilities
  • Display impersonation context in UI and activity feeds

1.4 Non-Goals

  • User-to-user impersonation (only bot-to-user)
  • Impersonation for authentication purposes
  • Bypassing authorization checks

2. Design Principles

  1. Security First: Only authorized bots can impersonate, with strict validation
  2. Transparency: All impersonated actions are clearly visible in audit logs and UI
  3. Permission Inheritance: Use impersonated user's permissions, not bot's
  4. Backward Compatibility: Existing entities without impersonation continue to work
  5. Auditability: Complete trail of who did what via which bot

3. Architecture

3.1 Token Exchange Flow (On-Demand Impersonation)

Since JWT tokens are pre-generated and bots cannot dynamically create tokens with different impersonatedUser claims, we need a token exchange API that allows bots to obtain short-lived impersonation tokens on-demand.

┌─────────────────────────────────────────────────────────┐
│ Bot Client (Ingestion/Automation)                       │
│                                                          │
│ 1. Bot has standard JWT token:                          │
│    - isBot: true                                         │
│    - sub: "ingestion-bot"                               │
│    - No impersonatedUser claim                          │
└─────────────────┬────────────────────────────────────────┘
                  │
                  │ POST /api/v1/users/impersonate
                  │ Authorization: Bearer <bot-token>
                  │ Body: { "targetUser": "alice" }
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ Token Exchange API                                       │
│                                                          │
│ 2. Validates bot token                                  │
│ 3. Checks bot has allowImpersonation=true               │
│ 4. Validates IMPERSONATE permission via policy          │
│ 5. Generates new JWT with:                              │
│    - isBot: true                                         │
│    - sub: "ingestion-bot"                               │
│    - impersonatedUser: "alice"                          │
│    - exp: short TTL (1 hour)                            │
│ 6. Returns impersonation token                          │
└─────────────────┬────────────────────────────────────────┘
                  │
                  │ Response: { "accessToken": "eyJ..." }
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ Bot Client                                               │
│                                                          │
│ 7. Uses impersonation token for API calls               │
│ 8. Token includes impersonatedUser claim                │
└─────────────────┬────────────────────────────────────────┘
                  │
                  │ PATCH /api/v1/tables/{id}
                  │ Authorization: Bearer <impersonation-token>
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ JwtFilter (Authentication)                              │
│                                                          │
│ 2. Validates JWT and extracts claims                    │
│ 3. Verifies: isBot == true                              │
│ 4. Checks bot has allowImpersonation flag               │
│ 5. Creates CatalogSecurityContext:                      │
│    - principal: "ingestion-bot"                         │
│    - impersonatedUser: "alice"                          │
└─────────────────┬────────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ DefaultAuthorizer (Authorization)                       │
│                                                          │
│ 6. Validates impersonation permission:                  │
│    - Check policy: bot can IMPERSONATE user?            │
│    - Can be restricted by team/domain                   │
│ 7. Creates SubjectContext for "alice"                   │
│ 8. Evaluates permissions using alice's context          │
└─────────────────┬────────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ EntityRepository (Persistence)                          │
│                                                          │
│ 9. Sets entity fields:                                  │
│    - updatedBy: "alice"                                 │
│    - impersonatedBy: "ingestion-bot"                    │
│    - updatedAt: current_timestamp                       │
│ 10. Stores to database                                  │
└─────────────────┬────────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────────────────┐
│ Database & Search Index                                 │
│                                                          │
│ entity_table:                                            │
│ ┌──────────┬────────┬───────────┬─────────────────┐    │
│ │ id       │ name   │ updatedBy │ impersonatedBy  │    │
│ ├──────────┼────────┼───────────┼─────────────────┤    │
│ │ uuid-123 │ table1 │ alice     │ ingestion-bot   │    │
│ └──────────┴────────┴───────────┴─────────────────┘    │
└─────────────────────────────────────────────────────────┘

3.2 Component Details

3.2.1 JWT Token Structure

Standard Token (No Impersonation)

{
  "sub": "alice",
  "email": "alice@example.com",
  "isBot": false,
  "exp": 1234567890
}

Impersonation Token

{
  "sub": "ingestion-bot",
  "email": "ingestion-bot@example.com",
  "isBot": true,
  "impersonatedUser": "alice",
  "exp": 1234567890
}

3.2.2 Security Context

CatalogSecurityContext Extension

public record CatalogSecurityContext(
    Principal principal,           // The bot
    String scheme,
    String authenticationScheme,
    Set<String> userRoles,
    boolean isBot,
    String impersonatedUser        // NEW: The actual user
) implements SecurityContext

SubjectContext Enhancement

public record SubjectContext(
    User user,                     // The impersonated user (or actual user)
    String impersonatedBy          // NEW: The bot name (if impersonating)
)

4. Authorization Model

4.1 Three-Level Security Check

Level 1: Bot Capability Flag

// User entity schema
{
  "name": "ingestion-bot",
  "isBot": true,
  "allowImpersonation": true  // NEW: Must be explicitly enabled
}

Level 2: Impersonation Permission

// New MetadataOperation enum value
public enum MetadataOperation {
    // ... existing operations
    IMPERSONATE  // NEW: Permission to impersonate users
}

Level 3: Policy-Based Control

{
  "name": "IngestionBotImpersonationPolicy",
  "description": "Allow ingestion bot to impersonate users in Engineering domain",
  "rules": [
    {
      "name": "ImpersonateEngineeringUsers",
      "resources": ["user"],
      "operations": ["Impersonate"],
      "effect": "allow",
      "condition": "hasDomain('Engineering')"
    }
  ]
}

4.2 Authorization Flow

// Pseudo-code for validation in JwtFilter
void validateImpersonation(String botName, String targetUser) {
    // 1. Bot must have isBot=true (already validated)

    // 2. Check bot has allowImpersonation flag
    User bot = Entity.getEntityByName(USER, botName, "allowImpersonation", NON_DELETED);
    if (!Boolean.TRUE.equals(bot.getAllowImpersonation())) {
        throw new AuthorizationException("Bot not authorized to impersonate");
    }

    // 3. Check policy grants IMPERSONATE permission
    SubjectContext botContext = SubjectContext.getSubjectContext(botName);
    User targetUserEntity = Entity.getEntityByName(USER, targetUser, "", NON_DELETED);
    ResourceContext resourceContext = new ResourceContext(USER, targetUserEntity.getId(), null);
    OperationContext operationContext = new OperationContext(USER, MetadataOperation.IMPERSONATE);

    // This throws AuthorizationException if not permitted
    PolicyEvaluator.hasPermission(botContext, resourceContext, operationContext);

    // 4. Validation passed - impersonation allowed
}

4.3 Permission Evaluation

When a bot impersonates a user, authorization checks use the impersonated user's permissions, not the bot's:

// In DefaultAuthorizer
public static SubjectContext getSubjectContext(SecurityContext securityContext) {
    CatalogSecurityContext catalogContext = (CatalogSecurityContext) securityContext;

    // Use impersonated user if present, otherwise use principal
    String userName = catalogContext.impersonatedUser() != null
        ? catalogContext.impersonatedUser()
        : catalogContext.getUserPrincipal().getName();

    User user = Entity.getEntityByName(Entity.USER, userName, USER_FIELDS, NON_DELETED);

    // Track who is impersonating (if applicable)
    String impersonatedBy = catalogContext.impersonatedUser() != null
        ? catalogContext.getUserPrincipal().getName()
        : null;

    return new SubjectContext(user, impersonatedBy);
}

Example: If ingestion-bot impersonates alice:

  • CRUD operations are authorized using alice's roles/teams/policies
  • If alice lacks permission to update a table, the operation fails
  • This prevents privilege escalation

4.4 Bot Impersonation Grants and RBAC Scoping (v1.1 — June 2026)

This section supersedes the parts of 4.1/4.2 that the initial implementation simplified, and addresses two issues:

  • OpenMetadata #28043: custom bots cannot enable impersonation. The allowImpersonation flag exists only on the user entity, is not part of createUser.json/createBot.json, and PATCHing it on the bot user fails (500 in 1.12.x). Only application bots (CreateApp.allowBotImpersonation) could ever receive the flag.
  • Collate #3581: bots with impersonation enabled can impersonate admin users. Admins cannot be restricted via RBAC, so this is privilege escalation. Blocking it outright breaks workflows that legitimately need it (e.g. AskCollate), so the control must be configurable.

4.4.1 Design Decision: Single Flag + RBAC Scope

The split of responsibility is:

Question Mechanism
Can this bot impersonate at all? allowImpersonation boolean on the bot user — an admin-granted capability, set at bot creation
Who can this bot impersonate? Standard policy evaluation of the Impersonate operation with the target user as the resource

A second boolean (allowAdminImpersonation) was considered and rejected: target scoping belongs in policies/rules, where it is already expressive (conditions, deny rules, teams, domains) and admin-manageable without code changes.

4.4.1.1 Why a dedicated flag and not RBAC alone

This question recurs ("Impersonate is already an operation — drop the flag, grant it through a role like everything else"). It has been evaluated and rejected. The flag and RBAC answer two different questions, and collapsing them opens a real privilege-escalation gap:

  • The flag is the enablement-authority gate: may this bot impersonate at all? — admin-only, set at bot creation, enforced in DefaultAuthorizer independently of RBAC.
  • RBAC is the target-scope gate: whom may it impersonate? — policies, conditions, deny rules.

If enablement were pure RBAC, "who can enable impersonation" would collapse into "who can grant the Impersonate operation" — i.e. anyone with EditRoles on a bot user or EditPolicy+Create on policies. That permission set is routinely delegated to non-admins (orgs delegate RBAC management). Impersonation — which lets a bot act as any user, including bypassing controls admins can't otherwise be restricted by — would then become grantable by non-admins. The flag keeps enablement admin-only regardless of how RBAC editing is delegated.

The flag is also the robust control, not merely an extra layer. Under RBAC-only, the Impersonate grant can arrive through many paths — a direct role, a team's defaultRoles the bot inherits, an inherited policy, or a broad ["All"]-operations policy (subsumption). Guaranteeing "admin-only" would mean guarding every one of those write paths. The flag is instead a single chokepoint: one admin-only, creation-time write, checked before any policy evaluation. It is belt-and-suspenders — even if Impersonate leaks into a bot's effective policy, no flag means no impersonation.

Two facts bound the surface and explain what the flag does and does not cover:

  • JwtFilter rejects the X-Impersonate-User header for any non-bot principal ("Only bot users can impersonate other users"). So a regular user self-granting Impersonate achieves nothing — they are not a bot. By default they also cannot edit RBAC. This case is already closed without the flag.
  • The case the flag uniquely closes is a non-admin RBAC delegate enabling impersonation on a bot. RBAC-only would permit it; the flag does not.

Conclusion: the flag is not redundant with RBAC — it is the admin-only, single-chokepoint enablement gate that RBAC delegation cannot widen. Do not remove it without replacing this property. The only redundancy ever identified was cosmetic: BotImpersonationRole is auto-attached when the flag is set, so the two travel together for UX. That convenience does not make the flag itself redundant.

4.4.2 Granting the Capability (createBot.json)

createBot.json gains an optional allowImpersonation boolean with tri-state semantics (no schema default, so an absent field is null):

  • null (absent) — keep the bot user's current value. Critical for PUT-based upserts (ingestion re-applies bots via PUT /v1/bots); an absent field must not silently revoke the grant.
  • true — grant. Admin-only, and only when the bot is being created. Enabling impersonation on an existing bot is rejected with 400: an existing bot's token is already distributed, and flipping the flag would silently upgrade every holder of that token. Granting at creation forces a new bot + new token + deliberate admin action.
  • false — revoke. Admin-only, allowed at any time (privilege reduction).

When the grant is applied, BotResource propagates the flag to the bot user entity (single source of truth — the authorizer reads it from the user) and attaches the seeded BotImpersonationRole to the bot user so the capability works out of the box. Revoking removes the flag and detaches that role.

User.allowImpersonation becomes effectively read-only on the user APIs:

  • PATCH /v1/users changing it → 400 user attribute allowImpersonation can't be modified (replaces the unhandled 500 from #28043).
  • PUT /v1/users always carries over the stored value (fixes a latent bug where any PUT on a bot user nulled the flag, because CreateUser has no such field).

4.4.3 Scoping Who Can Be Impersonated (policies)

checkImpersonationAuthorization in DefaultAuthorizer evaluates, on every impersonated request:

  1. The impersonating principal is a bot with allowImpersonation=true (capability gate, unchanged).
  2. PolicyEvaluator.hasPermission(botSubjectContext, targetUserResourceContext, OperationContext(user, IMPERSONATE)) — full policy evaluation, with the target user as the resource. This replaces the previous flat scan of role policies for the Impersonate operation, which ignored rule effect (a deny rule counted as allow) and could not discriminate targets.

Because this is the standard evaluation path, deny-overrides-allow, SpEL conditions, and compiled-rule caching all apply. Two new condition functions make target discrimination possible:

Function True when
isAdminUser() the resource (target user) is an admin
isBotUser() the resource (target user) is a bot

Existing functions compose for finer scoping: matchTeam() (team-scoped impersonation), hasDomain(), matchAnyTag().

The Impersonate operation is registered as a user-resource operation, so the policy editor offers it on the user resource and the functions appear in the condition dropdown (/v1/policies/functions).

Impersonate is explicit-grant-only: CompiledRule.matchOperation excludes it from ALL/EditAll/ViewAll subsumption. A broad god-mode policy (operations: ["All"]) therefore neither grants nor appears to grant impersonation; only a rule naming Impersonate does. With the flag in place this is defense-in-depth (the flag already gates enablement), but it keeps permission listings honest and prevents impersonation from silently riding along with broad policies.

4.4.4 Seed Policies and Defaults

  • BotImpersonationPolicy (new, the default): allow Impersonate on user, no deny rules. Backward-compatible by default: a freshly granted bot can impersonate any user, including admins, matching the pre-existing ApplicationBotImpersonationPolicy (All/allow) behavior. This is deliberate — silently denying admin impersonation would break workflows (e.g. AskCollate) that rely on it.
  • BotImpersonationRole (new): DefaultBotPolicy + BotImpersonationPolicy. Auto-attached at grant time.
  • BotNonAdminImpersonationPolicy (new, opt-in restriction): allow Impersonate on user + deny isAdminUser(). Admins who want to prevent a bot from impersonating admins attach this instead of the permissive default.
  • BotNonAdminImpersonationRole (new): DefaultBotPolicy + BotNonAdminImpersonationPolicy. The directly-assignable form of the opt-in restriction — swap a bot's BotImpersonationRole for this to deny admin targets.
  • ApplicationBotImpersonationPolicy (existing, All/allow) is unchanged: application bots created with allowBotImpersonation (e.g. AskCollate) keep their current behavior, including admin impersonation.

Restriction is therefore opt-in, not default — the "configurable policy control" Collate #3581 asks for, delivered without breaking any bot that currently impersonates admins. The isAdminUser() / isBotUser() condition functions and deny-overrides-allow semantics let admins author any narrower scope (per-team, per-domain, deny-bot, etc.).

No migration needed. All four impersonation seeds (BotImpersonationPolicy, BotNonAdminImpersonationPolicy, and their roles) are new in this change — no released version has them. Seed loading is insert-if-missing, so a fresh install and any upgrade from a prior release both get the current (permissive) JSON seeded directly. There is no prior on-disk state to convert, so no Flyway migration or startup reconciliation is required.

4.4.5 Threat-Model Delta

Threat Before After
Bot impersonates admin Allowed for any impersonation bot Allowed by default (backward-compatible); deniable opt-in via BotNonAdminImpersonationPolicy (isAdminUser() deny)
Bot impersonates another bot (token laundering) Allowed Allowed by default; deniable via an isBotUser() deny rule in a custom policy
Flag flipped on live bot via PATCH 500, undefined behavior (works on newer builds) 400, read-only; grant is creation-time via createBot
Flag silently wiped by PUT on bot user Yes (latent bug) PUT carries over stored value
deny rule with Impersonate op Counted as allow Honored (standard evaluator)
Non-admin RBAC delegate enables impersonation on a bot n/a Blocked — enablement is the admin-only flag, independent of RBAC editing (see 4.4.1.1)
Broad ["All"] policy grants impersonation by subsumption Would grant Impersonate excluded from ALL/EditAll/ViewAll subsumption; flag still required

5. Data Model Changes

5.1 JSON Schema Updates

5.1.1 Base Type Definition

// openmetadata-spec/src/main/resources/json/schema/type/basic.json
{
  "definitions": {
    "impersonatedBy": {
      "description": "Bot user that performed the action on behalf of the actual user.",
      "type": "string"
    }
  }
}

5.1.2 Entity Schema Pattern

// Example: openmetadata-spec/src/main/resources/json/schema/entity/data/table.json
{
  "properties": {
    "updatedBy": {
      "description": "User who made the update.",
      "type": "string"
    },
    "impersonatedBy": {
      "description": "Bot that performed the update on behalf of updatedBy user.",
      "$ref": "../../type/basic.json#/definitions/impersonatedBy"
    },
    "updatedAt": {
      "$ref": "../../type/basic.json#/definitions/timestamp"
    }
  }
}

5.1.3 User Schema for Bot Capability

// openmetadata-spec/src/main/resources/json/schema/entity/teams/user.json
{
  "properties": {
    "isBot": {
      "description": "When true, indicates this is a bot user.",
      "type": "boolean",
      "default": false
    },
    "allowImpersonation": {
      "description": "When true, this bot is allowed to impersonate users (subject to policy checks).",
      "type": "boolean",
      "default": false
    }
  }
}

5.2 Java Entity Interface

// openmetadata-spec/src/main/java/org/openmetadata/schema/EntityInterface.java
public interface EntityInterface {
    // ... existing methods

    String getUpdatedBy();
    void setUpdatedBy(String updatedBy);

    // NEW: Impersonation tracking
    default String getImpersonatedBy() {
        return null;
    }

    default void setImpersonatedBy(String botName) {
        /* no-op implementation to be overridden */
    }
}

5.3 Database Schema Migration

MySQL Migration

-- bootstrap/sql/migrations/native/1.7.0/mysql/schemaChanges.sql

-- Add impersonatedBy column to all entity tables
ALTER TABLE table_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE dashboard_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE pipeline_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE topic_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE container_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE database_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE database_schema_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE chart_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE report_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE metric_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE ml_model_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE glossary_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE glossary_term_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE tag_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE classification_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE policy_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE test_suite_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE test_case_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;
ALTER TABLE event_subscription_entity ADD COLUMN impersonatedBy VARCHAR(256) DEFAULT NULL;

-- Add allowImpersonation flag to user_entity
ALTER TABLE user_entity ADD COLUMN allowImpersonation BOOLEAN DEFAULT FALSE;

-- Add index for querying by impersonatedBy
CREATE INDEX idx_table_entity_impersonatedBy ON table_entity(impersonatedBy);
-- Repeat for high-traffic entities...

PostgreSQL Migration

-- bootstrap/sql/migrations/native/1.7.0/postgres/schemaChanges.sql

-- Same as MySQL with appropriate syntax
ALTER TABLE table_entity ADD COLUMN IF NOT EXISTS impersonatedBy VARCHAR(256);
-- ... etc

5.4 Elasticsearch/OpenSearch Index Mapping

// Update index mappings to include impersonatedBy
{
  "mappings": {
    "properties": {
      "updatedBy": {
        "type": "keyword"
      },
      "impersonatedBy": {
        "type": "keyword"
      },
      "updatedAt": {
        "type": "date"
      }
    }
  }
}

6. Implementation Details

6.1 Token Exchange API (New Endpoint)

API Specification:

POST /api/v1/users/impersonate
Authorization: Bearer <bot-token>
Content-Type: application/json

Request Body:
{
  "targetUser": "alice",
  "expirySeconds": 3600  // Optional, defaults to 1 hour, max 24 hours
}

Response:
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsIn...",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "impersonatedUser": "alice"
}

Implementation:

// openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java

@POST
@Path("/impersonate")
@Operation(
    operationId = "generateImpersonationToken",
    summary = "Generate impersonation token for bot",
    description = "Generate a short-lived JWT token that allows the bot to impersonate a specific user",
    responses = {
      @ApiResponse(
          responseCode = "200",
          description = "Impersonation token generated successfully",
          content = @Content(
              mediaType = APPLICATION_JSON,
              schema = @Schema(implementation = JWTAuthMechanism.class))),
      @ApiResponse(responseCode = "403", description = "Bot not authorized to impersonate"),
      @ApiResponse(responseCode = "404", description = "Target user not found")
    })
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
public Response generateImpersonationToken(
    @Context SecurityContext securityContext,
    @Valid ImpersonationRequest request) {

    // 1. Extract bot from security context
    String botName = SecurityUtil.getUserName(securityContext);
    CatalogSecurityContext catalogContext = (CatalogSecurityContext) securityContext;

    // 2. Validate this is a bot user
    if (!catalogContext.isBot()) {
        throw new AuthorizationException("Only bot users can generate impersonation tokens");
    }

    // 3. Get bot user and verify allowImpersonation flag
    User bot = repository.getByName(
        null,
        botName,
        new EntityUtil.Fields(Set.of("allowImpersonation")),
        Include.NON_DELETED
    );

    if (!Boolean.TRUE.equals(bot.getAllowImpersonation())) {
        throw new AuthorizationException(
            String.format("Bot '%s' is not authorized to impersonate users", botName)
        );
    }

    // 4. Validate target user exists
    String targetUser = request.getTargetUser();
    User user = repository.getByName(
        null,
        targetUser,
        new EntityUtil.Fields(Set.of("email", "roles", "teams", "isAdmin")),
        Include.NON_DELETED
    );

    // 5. Check IMPERSONATE permission via policy
    SubjectContext botContext = SubjectContext.getSubjectContext(botName);
    ResourceContext userResourceContext = new ResourceContext(
        Entity.USER,
        user.getId(),
        null
    );
    OperationContext impersonateOperation = new OperationContext(
        Entity.USER,
        MetadataOperation.IMPERSONATE
    );

    try {
        PolicyEvaluator.hasPermission(botContext, userResourceContext, impersonateOperation);
    } catch (AuthorizationException e) {
        throw new AuthorizationException(
            String.format("Bot '%s' is not authorized to impersonate user '%s'",
                botName, targetUser),
            e
        );
    }

    // 6. Generate impersonation token with short expiry
    long expirySeconds = request.getExpirySeconds() != null
        ? Math.min(request.getExpirySeconds(), 24 * 3600)  // Max 24 hours
        : 3600;  // Default 1 hour

    JWTAuthMechanism authMechanism = JWTTokenGenerator.getInstance()
        .generateImpersonationToken(
            botName,
            targetUser,
            getRoleListFromUser(bot),
            bot.getIsAdmin(),
            bot.getEmail(),
            expirySeconds
        );

    // 7. Audit log
    LOG.info("Impersonation token generated: bot={}, target={}, expirySeconds={}",
        botName, targetUser, expirySeconds);

    return Response.ok(authMechanism).build();
}

New Request Schema:

// openmetadata-spec/src/main/resources/json/schema/auth/impersonationRequest.json
{
  "$id": "https://open-metadata.org/schema/auth/impersonationRequest.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "ImpersonationRequest",
  "description": "Request to generate an impersonation token",
  "type": "object",
  "javaType": "org.openmetadata.schema.auth.ImpersonationRequest",
  "properties": {
    "targetUser": {
      "description": "Username of the user to impersonate",
      "type": "string"
    },
    "expirySeconds": {
      "description": "Token expiry in seconds (default 3600, max 86400)",
      "type": "integer",
      "minimum": 60,
      "maximum": 86400,
      "default": 3600
    }
  },
  "required": ["targetUser"],
  "additionalProperties": false
}

6.2 JWTTokenGenerator Enhancement

Add new method to generate impersonation tokens:

// openmetadata-service/src/main/java/org/openmetadata/service/security/jwt/JWTTokenGenerator.java

public class JWTTokenGenerator {
    public static final String IMPERSONATED_USER_CLAIM = "impersonatedUser";  // NEW

    // NEW: Generate impersonation token
    public JWTAuthMechanism generateImpersonationToken(
        String botName,
        String targetUser,
        Set<String> botRoles,
        boolean isBotAdmin,
        String botEmail,
        long expiryInSeconds
    ) {
        try {
            Algorithm algorithm = getAlgorithm(tokenValidationAlgorithm, null, privateKey);

            Date expiryDate = getCustomExpiryDate(expiryInSeconds);

            String token = JWT.create()
                .withSubject(botName)
                .withClaim(EMAIL_CLAIM, botEmail)
                .withClaim(IS_BOT_CLAIM, true)
                .withClaim(TOKEN_TYPE, ServiceTokenType.BOT.value())
                .withClaim(IMPERSONATED_USER_CLAIM, targetUser)  // NEW: Add impersonation claim
                .withExpiresAt(expiryDate)
                .withIssuer(issuer)
                .withKeyId(kid)
                .sign(algorithm);

            JWTAuthMechanism jwtAuthMechanism = new JWTAuthMechanism()
                .withJWTToken(token)
                .withJWTTokenExpiresAt(expiryDate.getTime());

            return jwtAuthMechanism;
        } catch (JWTCreationException e) {
            throw AuthenticationException.invalidToken("Failed to generate impersonation token", e);
        }
    }

    private Date getCustomExpiryDate(long expiryInSeconds) {
        return Date.from(
            LocalDateTime.now()
                .plusSeconds(expiryInSeconds)
                .atZone(ZoneId.systemDefault())
                .toInstant()
        );
    }
}

6.3 JwtFilter Enhancement

// openmetadata-service/src/main/java/org/openmetadata/service/security/JwtFilter.java

public class JwtFilter implements ContainerRequestFilter {
    public static final String IMPERSONATED_USER_CLAIM = "impersonatedUser";  // NEW

    @Override
    public void filter(ContainerRequestContext requestContext) {
        // ... existing code ...

        String tokenFromHeader = extractToken(requestContext.getHeaders());
        Map<String, Claim> claims = validateJwtAndGetClaims(tokenFromHeader);

        String userName = findUserNameFromClaims(jwtPrincipalClaimsMapping, jwtPrincipalClaims, claims);
        String email = findEmailFromClaims(jwtPrincipalClaimsMapping, jwtPrincipalClaims, claims, principalDomain);
        boolean isBotUser = isBot(claims);

        // NEW: Extract and validate impersonation
        String impersonatedUser = null;
        if (claims.containsKey(IMPERSONATED_USER_CLAIM)) {
            impersonatedUser = claims.get(IMPERSONATED_USER_CLAIM).asString();
            validateImpersonation(userName, impersonatedUser, isBotUser);
        }

        // Check existing validations
        checkValidationsForToken(claims, tokenFromHeader, userName);

        // Create security context with impersonation
        CatalogPrincipal catalogPrincipal = new CatalogPrincipal(userName, email);
        String scheme = requestContext.getUriInfo().getRequestUri().getScheme();

        CatalogSecurityContext catalogSecurityContext = new CatalogSecurityContext(
            catalogPrincipal,
            scheme,
            SecurityContext.DIGEST_AUTH,
            getUserRolesFromClaims(claims, isBotUser),
            isBotUser,
            impersonatedUser  // NEW
        );

        requestContext.setSecurityContext(catalogSecurityContext);
    }

    private void validateImpersonation(String botName, String targetUser, boolean isBot) {
        // 1. Only bots can impersonate
        if (!isBot) {
            throw new AuthorizationException("Only bot users can impersonate");
        }

        // 2. Check bot has allowImpersonation flag
        User bot = Entity.getEntityByName(Entity.USER, botName, "allowImpersonation", Include.NON_DELETED);
        if (!Boolean.TRUE.equals(bot.getAllowImpersonation())) {
            throw new AuthorizationException(
                String.format("Bot '%s' is not authorized to impersonate users", botName)
            );
        }

        // 3. Verify target user exists
        try {
            Entity.getEntityByName(Entity.USER, targetUser, "", Include.NON_DELETED);
        } catch (EntityNotFoundException e) {
            throw new AuthorizationException(
                String.format("Cannot impersonate non-existent user '%s'", targetUser)
            );
        }

        // 4. Check policy permission (done in DefaultAuthorizer during actual operation)
        // We defer this to avoid loading full context here
    }
}

6.3 DefaultAuthorizer Update

// openmetadata-service/src/main/java/org/openmetadata/service/security/DefaultAuthorizer.java

public class DefaultAuthorizer implements Authorizer {

    @Override
    public void authorize(
        SecurityContext securityContext,
        OperationContext operationContext,
        ResourceContextInterface resourceContext
    ) {
        CatalogSecurityContext catalogContext = (CatalogSecurityContext) securityContext;

        // NEW: If impersonating, verify impersonation permission first
        if (catalogContext.impersonatedUser() != null) {
            validateImpersonationPermission(
                catalogContext.getUserPrincipal().getName(),
                catalogContext.impersonatedUser()
            );
        }

        // Use impersonated user's context for authorization
        SubjectContext subjectContext = getSubjectContext(securityContext);

        if (subjectContext.isAdmin()) {
            return;
        }

        if (isReviewer(resourceContext, subjectContext)) {
            return;
        }

        PolicyEvaluator.hasPermission(subjectContext, resourceContext, operationContext);
    }

    // NEW: Validate impersonation permission
    private void validateImpersonationPermission(String botName, String targetUser) {
        SubjectContext botContext = SubjectContext.getSubjectContext(botName);
        User targetUserEntity = Entity.getEntityByName(
            Entity.USER,
            targetUser,
            "",
            Include.NON_DELETED
        );

        ResourceContext userResourceContext = new ResourceContext(
            Entity.USER,
            targetUserEntity.getId(),
            null
        );

        OperationContext impersonateOperation = new OperationContext(
            Entity.USER,
            MetadataOperation.IMPERSONATE
        );

        try {
            PolicyEvaluator.hasPermission(botContext, userResourceContext, impersonateOperation);
        } catch (AuthorizationException e) {
            throw new AuthorizationException(
                String.format("Bot '%s' is not authorized to impersonate user '%s'",
                    botName, targetUser),
                e
            );
        }
    }

    public static SubjectContext getSubjectContext(SecurityContext securityContext) {
        if (securityContext == null || securityContext.getUserPrincipal() == null) {
            throw new AuthenticationException("No principal in security context");
        }

        CatalogSecurityContext catalogContext = (CatalogSecurityContext) securityContext;

        // NEW: Use impersonated user if present
        String userName = catalogContext.impersonatedUser() != null
            ? catalogContext.impersonatedUser()
            : SecurityUtil.getUserName(securityContext);

        String impersonatedBy = catalogContext.impersonatedUser() != null
            ? catalogContext.getUserPrincipal().getName()
            : null;

        User user = Entity.getEntityByName(Entity.USER, userName, USER_FIELDS, Include.NON_DELETED);
        return new SubjectContext(user, impersonatedBy);
    }
}

6.4 EntityRepository Update

// openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java

public abstract class EntityRepository<T extends EntityInterface> {

    protected void prepareInternal(T entity, CreateEntity request, String updatedBy) {
        // ... existing code ...

        entity.setUpdatedBy(updatedBy);
        entity.setUpdatedAt(System.currentTimeMillis());

        // NEW: Set impersonatedBy from SubjectContext
        SubjectContext subjectContext = SubjectContext.getSubjectContext(updatedBy);
        if (subjectContext.impersonatedBy() != null) {
            entity.setImpersonatedBy(subjectContext.impersonatedBy());
        }

        // ... rest of existing code ...
    }

    public final PutResponse<T> update(UriInfo uriInfo, T original, T updated, String updatedBy) {
        setFieldsInternal(original, putFields);
        updated.setUpdatedBy(updatedBy);
        updated.setUpdatedAt(System.currentTimeMillis());

        // NEW: Set impersonatedBy
        SubjectContext subjectContext = SubjectContext.getSubjectContext(updatedBy);
        if (subjectContext.impersonatedBy() != null) {
            updated.setImpersonatedBy(subjectContext.impersonatedBy());
        }

        // ... rest of existing code ...
    }
}

7. Frontend Changes

7.1 Type Definitions (Auto-generated)

// Generated from schema
export interface EntityReference {
  id: string;
  name: string;
  fullyQualifiedName?: string;
  // ... other fields
}

export interface Table extends EntityInterface {
  // ... other fields
  updatedBy?: string;
  impersonatedBy?: string;  // NEW
  updatedAt?: number;
}

7.2 UI Components

Entity Header Component

// Display impersonation badge
const EntityHeader = ({ entity }: { entity: EntityInterface }) => {
  return (
    <div className="entity-header">
      <div className="updated-info">
        {entity.impersonatedBy ? (
          <Tooltip
            title={`Action performed by ${entity.updatedBy} via bot ${entity.impersonatedBy}`}
          >
            <span>
              Updated by <strong>{entity.updatedBy}</strong>
              <Tag color="blue" className="impersonation-tag">
                via {entity.impersonatedBy}
              </Tag>
            </span>
          </Tooltip>
        ) : (
          <span>Updated by <strong>{entity.updatedBy}</strong></span>
        )}
        <span className="timestamp">{formatDateTime(entity.updatedAt)}</span>
      </div>
    </div>
  );
};

Activity Feed

// Show impersonation in activity feed
const ActivityFeedItem = ({ activity }: { activity: ChangeEvent }) => {
  const displayName = activity.impersonatedBy
    ? `${activity.userName} (via ${activity.impersonatedBy})`
    : activity.userName;

  return (
    <div className="activity-item">
      <Avatar name={activity.userName} />
      <div>
        <strong>{displayName}</strong> {activity.action} {activity.entityType}
        <time>{formatRelativeTime(activity.timestamp)}</time>
      </div>
    </div>
  );
};

7.3 Admin Settings UI

// Bot configuration page
const BotSettings = ({ bot }: { bot: User }) => {
  const [allowImpersonation, setAllowImpersonation] = useState(bot.allowImpersonation);

  return (
    <div className="bot-settings">
      <h3>Impersonation Settings</h3>
      <Switch
        checked={allowImpersonation}
        onChange={(checked) => {
          updateBotSettings(bot.id, { allowImpersonation: checked });
          setAllowImpersonation(checked);
        }}
      />
      <p className="help-text">
        Allow this bot to impersonate users (subject to policy restrictions)
      </p>

      {allowImpersonation && (
        <Alert type="warning">
          This bot can perform actions on behalf of users. Ensure appropriate
          impersonation policies are configured.
        </Alert>
      )}
    </div>
  );
};

8. Usage Examples

8.1 Python SDK Usage

from metadata.ingestion.ometa.ometa_api import OpenMetadata
from metadata.generated.schema.entity.services.connections.metadata.openMetadataConnection import (
    OpenMetadataConnection,
    AuthProvider
)
from metadata.generated.schema.security.client.openMetadataJWTClientConfig import (
    OpenMetadataJWTClientConfig
)

# Initialize OM client with bot token
bot_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."  # Bot's standard JWT token

server_config = OpenMetadataConnection(
    hostPort="http://localhost:8585/api",
    authProvider=AuthProvider.openmetadata,
    securityConfig=OpenMetadataJWTClientConfig(jwtToken=bot_token)
)

metadata = OpenMetadata(server_config)

# Step 1: Request impersonation token for a specific user
impersonation_response = metadata.client.post(
    "/users/impersonate",
    data={
        "targetUser": "alice",
        "expirySeconds": 3600
    }
)

impersonation_token = impersonation_response["accessToken"]
print(f"Obtained impersonation token for alice: {impersonation_token[:50]}...")

# Step 2: Create new OM client with impersonation token
impersonation_config = OpenMetadataConnection(
    hostPort="http://localhost:8585/api",
    authProvider=AuthProvider.openmetadata,
    securityConfig=OpenMetadataJWTClientConfig(jwtToken=impersonation_token)
)

impersonation_metadata = OpenMetadata(impersonation_config)

# Step 3: Perform operations as alice (via bot)
from metadata.generated.schema.entity.data.table import Table

table = impersonation_metadata.get_by_name(
    entity=Table,
    fqn="sample_data.ecommerce_db.shopify.dim_customer"
)

table.description = "Updated by alice via ingestion-bot"

updated_table = impersonation_metadata.patch(table)

# Result:
# - updatedBy will be "alice"
# - impersonatedBy will be "ingestion-bot"
# - Authorization checks use alice's permissions

print(f"Updated table: {updated_table.fullyQualifiedName}")
print(f"Updated by: {updated_table.updatedBy}")
print(f"Impersonated by: {updated_table.impersonatedBy}")

8.2 Ingestion Workflow Example

# In ingestion connector code
class CustomIngestionSource(Source):
    def __init__(self, config, metadata_config):
        self.config = config
        self.metadata = OpenMetadata(metadata_config)
        self.impersonation_token = None
        self.impersonation_metadata = None

    def prepare(self):
        """Request impersonation token for the user who triggered ingestion"""
        if self.config.impersonateUser:
            response = self.metadata.client.post(
                "/users/impersonate",
                data={
                    "targetUser": self.config.impersonateUser,
                    "expirySeconds": 7200  # 2 hours for ingestion job
                }
            )
            self.impersonation_token = response["accessToken"]

            # Create new metadata client with impersonation
            impersonation_config = OpenMetadataConnection(
                hostPort=self.metadata.config.hostPort,
                authProvider=AuthProvider.openmetadata,
                securityConfig=OpenMetadataJWTClientConfig(
                    jwtToken=self.impersonation_token
                )
            )
            self.impersonation_metadata = OpenMetadata(impersonation_config)

    def yield_create_request(self, entity_data):
        """Use impersonation client if available"""
        client = self.impersonation_metadata or self.metadata

        # All operations will be attributed to impersonateUser
        # but tracked as performed by ingestion bot
        return CreateTableRequest(
            name=entity_data.name,
            description=entity_data.description,
            # ... other fields
        )

8.3 REST API Examples

Get Impersonation Token

curl -X POST https://localhost:8585/api/v1/users/impersonate \
  -H "Authorization: Bearer <bot-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "targetUser": "alice",
    "expirySeconds": 3600
  }'

# Response:
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik...",
  "tokenType": "Bearer",
  "expiresIn": 3600,
  "impersonatedUser": "alice",
  "jwtTokenExpiry": {
    "expiresAt": 1699123456789
  }
}

Use Impersonation Token

# Update table using impersonation token
IMPERSONATION_TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik..."

curl -X PATCH https://localhost:8585/api/v1/tables/{table-id} \
  -H "Authorization: Bearer $IMPERSONATION_TOKEN" \
  -H "Content-Type: application/json-patch+json" \
  -d '[{
    "op": "add",
    "path": "/description",
    "value": "Updated via impersonation"
  }]'

# Response includes:
{
  "id": "...",
  "name": "dim_customer",
  "updatedBy": "alice",
  "impersonatedBy": "ingestion-bot",
  "updatedAt": 1699120000000
}

Verify Impersonation in Activity Feed

curl -X GET "https://localhost:8585/api/v1/feed?entityLink=<#E::table::..." \
  -H "Authorization: Bearer <token>"

# Response shows:
{
  "data": [
    {
      "id": "...",
      "type": "entityUpdated",
      "userName": "alice",
      "impersonatedBy": "ingestion-bot",
      "timestamp": 1699120000000,
      "changeDescription": {
        "fieldsUpdated": [
          {
            "name": "description",
            "oldValue": "...",
            "newValue": "Updated via impersonation"
          }
        ]
      }
    }
  ]
}

8.4 Error Scenarios

Bot Without Impersonation Permission

curl -X POST https://localhost:8585/api/v1/users/impersonate \
  -H "Authorization: Bearer <bot-token-without-permission>" \
  -d '{"targetUser": "alice"}'

# Response: 403 Forbidden
{
  "code": 403,
  "message": "Bot 'my-bot' is not authorized to impersonate users",
  "exceptionType": "AuthorizationException"
}

Bot Without allowImpersonation Flag

# Response: 403 Forbidden
{
  "code": 403,
  "message": "Bot 'my-bot' is not authorized to impersonate users",
  "exceptionType": "AuthorizationException"
}

Policy Restriction Violation

# Bot tries to impersonate user from different domain
curl -X POST https://localhost:8585/api/v1/users/impersonate \
  -H "Authorization: Bearer <bot-token>" \
  -d '{"targetUser": "bob-from-marketing"}'

# Response: 403 Forbidden
{
  "code": 403,
  "message": "Bot 'engineering-bot' is not authorized to impersonate user 'bob-from-marketing'",
  "exceptionType": "AuthorizationException"
}

Non-Bot User Attempt

# Regular user tries to get impersonation token
curl -X POST https://localhost:8585/api/v1/users/impersonate \
  -H "Authorization: Bearer <regular-user-token>" \
  -d '{"targetUser": "alice"}'

# Response: 403 Forbidden
{
  "code": 403,
  "message": "Only bot users can generate impersonation tokens",
  "exceptionType": "AuthorizationException"
}

9. API Changes

9.1 REST API Behavior

All existing CRUD endpoints remain unchanged. Impersonation is handled transparently through JWT token claims.

9.2 New Endpoint Summary

Method Endpoint Description Auth Required
POST /api/v1/users/impersonate Request impersonation token Bot with allowImpersonation=true

8.2 Search and Filter Support

# Search for entities updated by specific user via bot
GET /api/v1/search/query?q=impersonatedBy:ingestion-bot

# Search for specific user's actions (including impersonated)
GET /api/v1/search/query?q=updatedBy:alice

# Activity feed with impersonation filter
GET /api/v1/feed?filterType=impersonated&bot=ingestion-bot

9. Security Considerations

9.1 Threat Model

Threat Mitigation
Unauthorized Impersonation Three-level validation: isBot + allowImpersonation flag + policy permission
Privilege Escalation Use impersonated user's permissions, not bot's
Token Theft Standard JWT security + short expiry + token validation
Audit Trail Tampering Immutable audit logs with both user and bot tracked
Policy Bypass Impersonation permission checked on every request

9.2 Security Best Practices

  1. Least Privilege: Only enable allowImpersonation for bots that absolutely need it
  2. Policy Restrictions: Use domain/team-based policies to limit scope
  3. Token Expiry: Impersonation tokens should have shorter expiry (e.g., 1 hour)
  4. Audit Monitoring: Alert on impersonation usage patterns
  5. Regular Review: Periodic audit of which bots have impersonation enabled

9.3 Attack Scenarios and Defenses

Scenario 1: Malicious Bot Attempts Impersonation

Attack: Bot without allowImpersonation tries to include impersonatedUser claim
Defense: JwtFilter validates allowImpersonation flag, rejects request
Result: 403 Forbidden - "Bot not authorized to impersonate"

Scenario 2: Bot Tries to Impersonate Admin

Attack: Bot with impersonation tries to impersonate admin user
Defense: Policy checks if bot can impersonate that specific user
Result: 403 Forbidden - "Bot 'X' not authorized to impersonate user 'admin'"

Scenario 3: Stolen Impersonation Token

Attack: Attacker obtains valid impersonation token
Defense:
  - Short token expiry (1 hour)
  - Token bound to specific user
  - Audit logs show suspicious activity patterns
  - Can revoke bot's allowImpersonation flag
Result: Limited blast radius, quick detection and mitigation

10. Testing Strategy

10.1 Unit Tests

// JwtFilterTest.java
@Test
public void testImpersonationValidation_Success() {
    // Given: Bot with allowImpersonation=true and valid token
    String token = generateImpersonationToken("test-bot", "alice");

    // When: Filter processes request
    CatalogSecurityContext context = jwtFilter.getCatalogSecurityContext(token);

    // Then: Context contains impersonation info
    assertEquals("test-bot", context.getUserPrincipal().getName());
    assertEquals("alice", context.impersonatedUser());
}

@Test
public void testImpersonationValidation_BotNotAuthorized() {
    // Given: Bot with allowImpersonation=false
    User bot = createBot("test-bot", false);
    String token = generateImpersonationToken("test-bot", "alice");

    // When/Then: Validation fails
    assertThrows(AuthorizationException.class, () -> {
        jwtFilter.validateJwtAndGetClaims(token);
    });
}

@Test
public void testNonBotCannotImpersonate() {
    // Given: Regular user tries to impersonate
    String token = JWT.create()
        .withSubject("alice")
        .withClaim("isBot", false)
        .withClaim("impersonatedUser", "bob")
        .sign(algorithm);

    // When/Then: Validation fails
    assertThrows(AuthorizationException.class, () -> {
        jwtFilter.validateJwtAndGetClaims(token);
    });
}
// EntityRepositoryTest.java
@Test
public void testEntityUpdate_WithImpersonation() {
    // Given: Bot impersonating user
    SubjectContext context = new SubjectContext(
        createUser("alice"),
        "ingestion-bot"  // impersonatedBy
    );

    Table table = createTable();

    // When: Update entity
    tableRepository.update(null, table, updatedTable, "alice");

    // Then: Both fields are set
    assertEquals("alice", updatedTable.getUpdatedBy());
    assertEquals("ingestion-bot", updatedTable.getImpersonatedBy());
}

10.2 Integration Tests

// ImpersonationIntegrationTest.java
@Test
public void testEndToEndImpersonation() {
    // 1. Create bot with impersonation enabled
    User bot = createBot("test-bot", true);
    bot.setAllowImpersonation(true);

    // 2. Create policy allowing impersonation
    Policy policy = createImpersonationPolicy("test-bot", "alice");

    // 3. Generate impersonation token
    String token = generateImpersonationToken("test-bot", "alice");

    // 4. Make API call to update entity
    Response response = updateTable(tableId, token, newDescription);

    // 5. Verify response
    assertEquals(200, response.getStatus());

    // 6. Verify database state
    Table updated = getTable(tableId);
    assertEquals("alice", updated.getUpdatedBy());
    assertEquals("test-bot", updated.getImpersonatedBy());

    // 7. Verify audit log
    List<ChangeEvent> events = getChangeEvents(tableId);
    assertEquals("alice", events.get(0).getUserName());
    assertEquals("test-bot", events.get(0).getImpersonatedBy());
}

@Test
public void testImpersonationWithoutPolicy_Fails() {
    // Setup: Bot with flag but no policy
    User bot = createBot("test-bot", true);
    String token = generateImpersonationToken("test-bot", "alice");

    // When: Attempt operation
    Response response = updateTable(tableId, token, newDescription);

    // Then: Fails with 403
    assertEquals(403, response.getStatus());
    assertTrue(response.getEntity().toString().contains("not authorized to impersonate"));
}

10.3 Security Tests

@Test
public void testPrivilegeEscalation_Prevented() {
    // Given: alice can edit tables in Engineering domain
    //        bob can only view tables
    //        bot impersonates bob

    User alice = createUser("alice", "Engineering", "DataSteward");
    User bob = createUser("bob", "Engineering", "DataConsumer");
    User bot = createBot("test-bot", true);

    Table table = createTableInDomain("Engineering");

    // When: Bot impersonates bob and tries to update table
    String token = generateImpersonationToken("test-bot", "bob");
    Response response = updateTable(table.getId(), token, "New description");

    // Then: Operation fails (bob lacks permission)
    assertEquals(403, response.getStatus());
}

@Test
public void testCrossDomainImpersonation_Restricted() {
    // Given: Policy allows impersonation only within Engineering domain
    User bot = createBot("test-bot", true);
    createDomainRestrictedImpersonationPolicy("test-bot", "Engineering");

    User engineeringUser = createUserInDomain("alice", "Engineering");
    User marketingUser = createUserInDomain("bob", "Marketing");

    // When: Try to impersonate within domain
    String token1 = generateImpersonationToken("test-bot", "alice");
    Response response1 = updateEntity(token1);
    assertEquals(200, response1.getStatus());  // Success

    // When: Try to impersonate outside domain
    String token2 = generateImpersonationToken("test-bot", "bob");
    Response response2 = updateEntity(token2);
    assertEquals(403, response2.getStatus());  // Failure
}

10.4 Performance Tests

@Test
public void testImpersonationOverhead() {
    // Measure overhead of impersonation validation

    // Baseline: Normal request
    long baseline = measureRequestTime(() -> updateTableNormal());

    // With impersonation
    long withImpersonation = measureRequestTime(() -> updateTableWithImpersonation());

    // Assert: Overhead < 10%
    double overhead = (withImpersonation - baseline) / (double) baseline;
    assertTrue(overhead < 0.10, "Impersonation overhead should be < 10%");
}

11. Migration Plan

Phase 1: Schema and Backend (Week 1-2)

  1. Add JSON schema definitions
  2. Add database migrations
  3. Update EntityInterface
  4. Regenerate Java/Python/TypeScript models
  5. Update JwtFilter, CatalogSecurityContext, SubjectContext
  6. Deploy to dev environment

Phase 2: Authorization (Week 2-3)

  1. Add IMPERSONATE to MetadataOperation enum
  2. Update DefaultAuthorizer with validation logic
  3. Update EntityRepository to populate impersonatedBy
  4. Add unit tests and integration tests
  5. Deploy to staging environment

Phase 3: Bot Support (Week 3-4)

  1. Add allowImpersonation to User schema
  2. Update bot creation APIs
  3. Update Python SDK to support impersonation tokens
  4. Update ingestion framework
  5. Test with sample ingestion workflows

Phase 4: Frontend (Week 4-5)

  1. Update UI components to display impersonation
  2. Add bot configuration page
  3. Update activity feed
  4. Add filtering by impersonatedBy

Phase 5: Documentation and Rollout (Week 5-6)

  1. Write user documentation
  2. Write admin guide for policy configuration
  3. Create migration guide for bot users
  4. Gradual rollout to production
  5. Monitor and adjust policies

12. Monitoring and Observability

12.1 Metrics

// Metrics to track
- impersonation.requests.total (counter)
- impersonation.requests.by_bot (counter, labeled by bot name)
- impersonation.requests.by_target_user (counter, labeled by user)
- impersonation.validation.failures (counter, labeled by reason)
- impersonation.authorization.duration (histogram)

12.2 Logging

// Log examples
LOG.info("Impersonation validated: bot={}, target={}, operation={}",
    botName, targetUser, operation);

LOG.warn("Impersonation attempt blocked: bot={}, target={}, reason={}",
    botName, targetUser, reason);

LOG.debug("Impersonation token processed: bot={}, user={}, entity={}",
    botName, userName, entityType);

12.3 Alerts

# Alert on suspicious patterns
- name: HighImpersonationFailureRate
  condition: impersonation.validation.failures > 10/min
  severity: warning

- name: UnauthorizedImpersonationAttempts
  condition: impersonation.requests{reason="unauthorized"} > 5/min
  severity: critical

- name: CrossDomainImpersonationSpike
  condition: impersonation.requests{cross_domain="true"} > 20/min
  severity: warning

13. Documentation

13.1 User Guide

For Bot Users:

  • How to enable impersonation for your bot
  • How to generate impersonation tokens
  • Examples in Python SDK and REST API
  • Troubleshooting common issues

For Admins:

  • How to configure impersonation policies
  • Best practices for security
  • How to audit impersonation usage
  • How to investigate suspicious activity

13.2 API Documentation

Update OpenAPI specification:

components:
  securitySchemes:
    BotImpersonation:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT token with impersonation claim. Bot must have allowImpersonation=true
        and policy permission to impersonate the target user.

        Token claims:
        - isBot: true (required)
        - impersonatedUser: string (target username)

14. Future Enhancements

14.1 Impersonation Audit Report

  • UI dashboard showing all impersonated actions
  • Exportable audit reports
  • Anomaly detection for unusual patterns

14.2 Temporary Impersonation Tokens

  • Time-limited impersonation grants
  • Automatic expiry after specific duration
  • Revocable impersonation sessions

14.3 Impersonation Approval Workflow

  • Require admin approval for sensitive impersonations
  • Approval via UI or API
  • Notification to impersonated user

14.4 Delegation Instead of Impersonation

  • User explicitly delegates actions to bot
  • Delegation scope limited to specific operations/entities
  • User can revoke delegation anytime

15. Appendix

  • AWS IAM Role Assumption
  • Kubernetes Service Account Impersonation
  • Google Cloud Service Account Impersonation

15.2 Security Standards

  • OWASP API Security Top 10
  • NIST 800-53 Access Control Guidelines
  • SOC 2 Audit Requirements

15.3 Glossary

  • Impersonation: Bot acting on behalf of a user
  • Subject: The entity performing authorization checks (user or bot)
  • Principal: The authenticated entity in security context
  • Policy: Rule defining what operations are allowed
  • Resource Context: Entity being accessed
  • Operation Context: Type of action being performed

Document Version: 1.1 Last Updated: 2026-06-12 Authors: OpenMetadata Engineering v1.1: Custom-bot impersonation grants via createBot.json, RBAC target scoping (isAdminUser()/isBotUser() conditions), read-only User.allowImpersonation on user APIs, Impersonate excluded from ALL subsumption. Default BotImpersonationPolicy is permissive/backward-compatible (admin impersonation stays allowed); admin restriction is opt-in via BotNonAdminImpersonationPolicy/BotNonAdminImpersonationRole. All seeds are new, so no migration is needed. See section 4.4. Status: Draft for Review