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
- Security First: Only authorized bots can impersonate, with strict validation
- Transparency: All impersonated actions are clearly visible in audit logs and UI
- Permission Inheritance: Use impersonated user's permissions, not bot's
- Backward Compatibility: Existing entities without impersonation continue to work
- 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
allowImpersonationflag exists only on theuserentity, is not part ofcreateUser.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
DefaultAuthorizerindependently 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:
JwtFilterrejects theX-Impersonate-Userheader for any non-bot principal ("Only bot users can impersonate other users"). So a regular user self-grantingImpersonateachieves 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 viaPUT /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/userschanging it → 400user attribute allowImpersonation can't be modified(replaces the unhandled 500 from #28043).PUT /v1/usersalways carries over the stored value (fixes a latent bug where any PUT on a bot user nulled the flag, becauseCreateUserhas no such field).
4.4.3 Scoping Who Can Be Impersonated (policies)
checkImpersonationAuthorization in DefaultAuthorizer evaluates, on every impersonated request:
- The impersonating principal is a bot with
allowImpersonation=true(capability gate, unchanged). 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 theImpersonateoperation, which ignored ruleeffect(adenyrule 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): allowImpersonateonuser, no deny rules. Backward-compatible by default: a freshly granted bot can impersonate any user, including admins, matching the pre-existingApplicationBotImpersonationPolicy(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): allowImpersonateonuser+ denyisAdminUser(). 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'sBotImpersonationRolefor this to deny admin targets.ApplicationBotImpersonationPolicy(existing,All/allow) is unchanged: application bots created withallowBotImpersonation(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
- Least Privilege: Only enable
allowImpersonationfor bots that absolutely need it - Policy Restrictions: Use domain/team-based policies to limit scope
- Token Expiry: Impersonation tokens should have shorter expiry (e.g., 1 hour)
- Audit Monitoring: Alert on impersonation usage patterns
- 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)
- Add JSON schema definitions
- Add database migrations
- Update EntityInterface
- Regenerate Java/Python/TypeScript models
- Update JwtFilter, CatalogSecurityContext, SubjectContext
- Deploy to dev environment
Phase 2: Authorization (Week 2-3)
- Add IMPERSONATE to MetadataOperation enum
- Update DefaultAuthorizer with validation logic
- Update EntityRepository to populate impersonatedBy
- Add unit tests and integration tests
- Deploy to staging environment
Phase 3: Bot Support (Week 3-4)
- Add allowImpersonation to User schema
- Update bot creation APIs
- Update Python SDK to support impersonation tokens
- Update ingestion framework
- Test with sample ingestion workflows
Phase 4: Frontend (Week 4-5)
- Update UI components to display impersonation
- Add bot configuration page
- Update activity feed
- Add filtering by impersonatedBy
Phase 5: Documentation and Rollout (Week 5-6)
- Write user documentation
- Write admin guide for policy configuration
- Create migration guide for bot users
- Gradual rollout to production
- 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
15.1 Related Work
- 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