Guide · AWS Lake Formation · LF-Tags
MCP Server Lake Formation LF-Tags — TBAC, CreateLFTagCommand, tag expressions
LF-tags (Lake Formation tag-based access control, or TBAC) replace per-resource permission grants with a tag-matching system: you define key+value tags, attach them to databases and tables, and grant IAM principals access to any resource that matches a tag expression. From an MCP server you create tags with CreateLFTagCommand, assign them to resources with AddLFTagsToResourceCommand, and issue permissions via GrantPermissionsCommand using the LFTagPolicy resource type. The critical insight is that an LFTagPolicy grant is dynamic — when you tag a new table with environment=prod, any principal that holds a grant on LFTagPolicy { Expression: [{ TagKey: 'environment', TagValues: ['prod'] }] } automatically gains access to that table without a new GrantPermissionsCommand call.
TL;DR
Create a tag key with allowed values using CreateLFTagCommand({ TagKey: 'environment', TagValues: ['prod', 'staging'] }). Assign it to a table: AddLFTagsToResourceCommand({ Resource: { Table: { DatabaseName, Name } }, LFTags: [{ TagKey: 'environment', TagValues: ['prod'] }] }). Grant a role access to all prod tables: GrantPermissionsCommand({ Resource: { LFTagPolicy: { ResourceType: 'TABLE', Expression: [{ TagKey: 'environment', TagValues: ['prod'] }] } }, Permissions: ['SELECT'] }). Any table later tagged environment=prod is instantly accessible to that role — no new grant needed.
How LF-tags work
An LF-tag has a key (a string name like environment or data-classification) and a list of allowed values (the set of valid strings like ['prod', 'staging', 'dev']). You assign one or more values from the allowed set to a Glue database, table, or column. Then instead of granting permissions directly to resources, you grant permissions on a tag expression — a list of tag conditions that the resource must satisfy.
| Concept | Example | Notes |
|---|---|---|
| Tag key | environment | Created with CreateLFTagCommand; unique per account+region+catalog |
| Tag value | prod | Must be in the tag's allowed values list; assigned to resources |
| Tag expression (AND) | [{ TagKey: 'env', TagValues: ['prod'] }, { TagKey: 'tier', TagValues: ['gold'] }] | Both conditions must match — resource must have env=prod AND tier=gold |
| Tag expression (OR within key) | [{ TagKey: 'env', TagValues: ['prod', 'staging'] }] | Resource must have env=prod OR env=staging |
| ResourceType | 'DATABASE' or 'TABLE' | Determines which Glue objects are matched by the expression |
When you tag a database with environment=prod, that tag propagates to all tables in the database as a parent tag. Tables can override the inherited tag by setting their own value. This propagation means you can tag once at the database level and have permissions apply automatically to all current and future tables in that database.
Creating and managing LF-tag keys
CreateLFTagCommand defines a new tag key with its set of allowed values. UpdateLFTagCommand adds or removes values from an existing key. You cannot remove a value that is currently assigned to any resource — Lake Formation returns ConstraintViolationException if you try.
import {
LakeFormationClient,
CreateLFTagCommand,
UpdateLFTagCommand,
DeleteLFTagCommand,
GetLFTagCommand,
ListLFTagsCommand,
} from '@aws-sdk/client-lakeformation';
const lakeFormation = new LakeFormationClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
export async function createTag(
key: string,
values: string[],
): Promise<void> {
await lakeFormation.send(new CreateLFTagCommand({
TagKey: key, // e.g. 'environment'
TagValues: values, // e.g. ['prod', 'staging', 'dev']
}));
}
// Add new allowed values to an existing tag
export async function addTagValues(
key: string,
newValues: string[],
): Promise<void> {
await lakeFormation.send(new UpdateLFTagCommand({
TagKey: key,
TagValuesToAdd: newValues,
// TagValuesToDelete: [] — omit to leave existing values unchanged
}));
}
// Remove allowed values (fails if any resource still uses that value)
export async function removeTagValues(
key: string,
valuesToRemove: string[],
): Promise<void> {
await lakeFormation.send(new UpdateLFTagCommand({
TagKey: key,
TagValuesToDelete: valuesToRemove,
}));
}
export async function getTag(key: string) {
const response = await lakeFormation.send(new GetLFTagCommand({ TagKey: key }));
return { key: response.TagKey, values: response.TagValues };
}
export async function listAllTags() {
const tags = [];
let nextToken: string | undefined;
do {
const response = await lakeFormation.send(
new ListLFTagsCommand({ NextToken: nextToken, ResourceShareType: 'ALL' })
);
tags.push(...(response.LFTags ?? []));
nextToken = response.NextToken;
} while (nextToken);
return tags;
}
DeleteLFTagCommand permanently removes a tag key and all its assignments from every resource. This is irreversible and cannot be undone. Any LFTagPolicy grants that reference this key become orphaned — they remain in the system but match nothing. Remove all dependent grants before deleting a tag key.
export async function deleteTag(key: string): Promise<void> {
// Warning: this is irreversible and silently orphans LFTagPolicy grants
// that reference this key. Revoke those grants first.
await lakeFormation.send(new DeleteLFTagCommand({ TagKey: key }));
}
Assigning tags to resources
AddLFTagsToResourceCommand assigns one or more tag key+value pairs to a Glue database, table, or set of columns. The value you assign must be in the tag's allowed values list — Lake Formation rejects values not in the list.
import {
AddLFTagsToResourceCommand,
RemoveLFTagsFromResourceCommand,
GetResourceLFTagsCommand,
} from '@aws-sdk/client-lakeformation';
// Tag a database (tags propagate to all tables as parent tags)
export async function tagDatabase(
databaseName: string,
tags: Array<{ key: string; value: string }>,
): Promise<void> {
await lakeFormation.send(new AddLFTagsToResourceCommand({
Resource: {
Database: { Name: databaseName },
},
LFTags: tags.map(t => ({
TagKey: t.key,
TagValues: [t.value], // assign exactly one value per key per resource
})),
}));
}
// Tag a specific table
export async function tagTable(
databaseName: string,
tableName: string,
tags: Array<{ key: string; value: string }>,
): Promise<void> {
await lakeFormation.send(new AddLFTagsToResourceCommand({
Resource: {
Table: { DatabaseName: databaseName, Name: tableName },
},
LFTags: tags.map(t => ({ TagKey: t.key, TagValues: [t.value] })),
}));
}
// Get all tags assigned to a resource (including inherited parent tags)
export async function getResourceTags(
databaseName: string,
tableName?: string,
) {
const resource = tableName
? { Table: { DatabaseName: databaseName, Name: tableName } }
: { Database: { Name: databaseName } };
const response = await lakeFormation.send(new GetResourceLFTagsCommand({
Resource: resource,
ShowAssignedLFTags: true, // include inherited tags from parent databases
}));
return {
// Tags directly assigned to this resource
lf_tags_on_database: response.LFTagsOnDatabase,
lf_tags_on_table: response.LFTagsOnTable,
lf_tags_on_columns: response.LFTagsOnColumns,
};
}
// Remove a tag from a resource
export async function removeTagFromTable(
databaseName: string,
tableName: string,
tagKey: string,
): Promise<void> {
// First get the current value so we know what to remove
const current = await lakeFormation.send(new GetResourceLFTagsCommand({
Resource: { Table: { DatabaseName: databaseName, Name: tableName } },
ShowAssignedLFTags: true,
}));
const tableTag = current.LFTagsOnTable?.find(t => t.TagKey === tagKey);
if (!tableTag) return; // tag not assigned to this resource
await lakeFormation.send(new RemoveLFTagsFromResourceCommand({
Resource: { Table: { DatabaseName: databaseName, Name: tableName } },
LFTags: [{ TagKey: tagKey, TagValues: tableTag.TagValues ?? [] }],
}));
}
Granting permissions via LFTagPolicy
A LFTagPolicy grant gives a principal access to any resource whose tags match the expression. The ResourceType field ('DATABASE' or 'TABLE') scopes the match to Glue databases or tables respectively. Matching is dynamic — newly tagged resources are automatically included without requiring a new grant.
import { GrantPermissionsCommand } from '@aws-sdk/client-lakeformation';
// Grant SELECT on all prod tables to an analytics role
export async function grantProdTablesSelect(
principalArn: string,
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
LFTagPolicy: {
ResourceType: 'TABLE',
Expression: [
{
TagKey: 'environment',
TagValues: ['prod'],
// Matches any table tagged environment=prod
},
],
},
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
}
// Multi-condition expression: prod AND gold tier (both must match — AND logic)
export async function grantProdGoldTablesSelect(
principalArn: string,
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
LFTagPolicy: {
ResourceType: 'TABLE',
Expression: [
{ TagKey: 'environment', TagValues: ['prod'] }, // AND
{ TagKey: 'tier', TagValues: ['gold'] }, // AND
// Both conditions must match — this is NOT an OR
],
},
},
Permissions: ['SELECT'],
PermissionsWithGrantOption: [],
}));
}
// OR within a single key: prod OR staging
export async function grantProdOrStagingSelect(
principalArn: string,
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
LFTagPolicy: {
ResourceType: 'TABLE',
Expression: [
{
TagKey: 'environment',
TagValues: ['prod', 'staging'],
// Multiple values in a single entry = OR within that key
},
],
},
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
}
The expression evaluation rule: within a single LFTagExpression entry, multiple TagValues are evaluated as OR. Multiple entries in the Expression array are evaluated as AND. So [{ key: 'env', values: ['prod', 'staging'] }, { key: 'tier', values: ['gold'] }] means: (env=prod OR env=staging) AND tier=gold.
Revoking and listing tag-based grants
Use RevokePermissionsCommand with the same LFTagPolicy structure to revoke a tag-based grant. Use ListPermissionsCommand with Resource: { LFTagPolicy: {...} } to find which principals hold grants on a specific tag expression — but note that this requires specifying the exact expression; there is no "list all LFTagPolicy grants" API that returns all expressions without filtering.
import {
RevokePermissionsCommand,
ListPermissionsCommand,
} from '@aws-sdk/client-lakeformation';
export async function revokeProdTablesSelect(principalArn: string): Promise<void> {
await lakeFormation.send(new RevokePermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
LFTagPolicy: {
ResourceType: 'TABLE',
Expression: [{ TagKey: 'environment', TagValues: ['prod'] }],
},
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
}
// List all principals with a tag-based grant on prod tables
export async function listProdTableGrantees() {
const grants = [];
let nextToken: string | undefined;
do {
const response = await lakeFormation.send(new ListPermissionsCommand({
Resource: {
LFTagPolicy: {
ResourceType: 'TABLE',
Expression: [{ TagKey: 'environment', TagValues: ['prod'] }],
},
},
NextToken: nextToken,
}));
grants.push(...(response.PrincipalResourcePermissions ?? []));
nextToken = response.NextToken;
} while (nextToken);
return grants;
}
IAM reference for LF-tag operations
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LFTagManagement",
"Effect": "Allow",
"Action": [
"lakeformation:CreateLFTag",
"lakeformation:UpdateLFTag",
"lakeformation:DeleteLFTag",
"lakeformation:GetLFTag",
"lakeformation:ListLFTags",
"lakeformation:AddLFTagsToResource",
"lakeformation:RemoveLFTagsFromResource",
"lakeformation:GetResourceLFTags",
"lakeformation:SearchTablesByLFTags",
"lakeformation:SearchDatabasesByLFTags"
],
"Resource": "*"
},
{
"Sid": "LFTagPolicyGrants",
"Effect": "Allow",
"Action": [
"lakeformation:GrantPermissions",
"lakeformation:RevokePermissions",
"lakeformation:ListPermissions"
],
"Resource": "*"
}
]
}
Error handling reference
| Error | Cause | Fix |
|---|---|---|
AlreadyExistsException | CreateLFTagCommand with a key that already exists | Use GetLFTagCommand first to check; use UpdateLFTagCommand to add values to existing key |
EntityNotFoundException | Tag key referenced in AddLFTagsToResourceCommand does not exist | Create the tag key first with CreateLFTagCommand |
InvalidInputException | Tag value not in the allowed values list for that key | Use GetLFTagCommand to verify allowed values before assigning; use UpdateLFTagCommand to add new values |
ConstraintViolationException | DeleteLFTagCommand or UpdateLFTagCommand removing a value still assigned to resources | Use SearchTablesByLFTagsCommand to find resources using that value and RemoveLFTagsFromResourceCommand on each before deleting |
AccessDeniedException | Missing lakeformation:CreateLFTag or lakeformation:AddLFTagsToResource | Add required actions to MCP server IAM policy; only data lake admins can create/delete tags by default |
Related pages
- Lake Formation overview — RegisterResourceCommand, data lake settings — registering S3 resources and configuring data lake admins
- Lake Formation permissions — GrantPermissionsCommand, column security — direct resource-level permission grants
- Lake Formation governed tables — StartTransactionCommand, UpdateTableObjects — ACID transactions on data lake objects
- AWS Glue Data Catalog for MCP servers