Guide · AWS Lake Formation · Permissions
MCP Server Lake Formation Permissions — GrantPermissionsCommand, column security
AWS Lake Formation's permission model governs who can read or write which databases, tables, columns, and S3 paths in your data lake. From an MCP server you call GrantPermissionsCommand to grant access, RevokePermissionsCommand to remove it, and ListPermissionsCommand to audit what access a principal has. The two things that trip up first-time callers are the resource type (the shape of the Resource object determines which permission set is valid — a Table resource uses SELECT/INSERT/DELETE while a Database resource uses CREATE_TABLE/DESCRIBE/ALTER/DROP) and column-level security (you must use the TableWithColumns resource type with either an explicit ColumnNames list or a ColumnWildcard with ExcludedColumnNames to restrict which columns a principal can read).
TL;DR
Call GrantPermissionsCommand with Principal.DataLakePrincipalIdentifier (an IAM role or user ARN), a typed Resource object (Catalog / Database / Table / TableWithColumns / DataLocation), and a Permissions array. For column-level security, use Resource: { TableWithColumns: { DatabaseName, Name, ColumnNames: ['col1'] } } instead of Table. Use BatchGrantPermissionsCommand to issue multiple grants in one API call. Audit with ListPermissionsCommand.
Resource types and valid permissions
The shape of the Resource field determines which permission strings are accepted. Using a permission not applicable to a resource type returns InvalidInputException.
| Resource type | Valid permissions | Notes |
|---|---|---|
Catalog: {} | CREATE_DATABASE | Grants permission to create databases in this account's Glue catalog |
Database: { Name } | CREATE_TABLE, DESCRIBE, ALTER, DROP, SUPER | SUPER grants all database-level permissions; Name is the Glue database name |
Table: { DatabaseName, Name } | SELECT, INSERT, DELETE, DESCRIBE, ALTER, DROP, SUPER | Use TableWildcard instead of Name to cover all tables in a database |
TableWithColumns: { DatabaseName, Name, ColumnNames } | SELECT, DESCRIBE | Column-level grant — only SELECT and DESCRIBE are supported on column resources |
DataLocation: { ResourceArn } | DATA_LOCATION_ACCESS | Grants permission to call RegisterResourceCommand on this S3 path |
Note that SUPER at the table level grants all of SELECT + INSERT + DELETE + DESCRIBE + ALTER + DROP. SUPER at the database level grants all database-level operations. Prefer explicit permission sets over SUPER in production — it makes auditing via ListPermissionsCommand harder to interpret.
Granting permissions with GrantPermissionsCommand
GrantPermissionsCommand is an idempotent upsert — granting a permission that already exists does not error. The PermissionsWithGrantOption array controls whether the grantee can further grant those permissions to other principals (the equivalent of SQL WITH GRANT OPTION).
import {
LakeFormationClient,
GrantPermissionsCommand,
type GrantPermissionsCommandInput,
} from '@aws-sdk/client-lakeformation';
const lakeFormation = new LakeFormationClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
// Grant SELECT on a specific table
export async function grantTableSelect(
principalArn: string,
databaseName: string,
tableName: string,
): Promise<void> {
const input: GrantPermissionsCommandInput = {
Principal: {
DataLakePrincipalIdentifier: principalArn,
// e.g. 'arn:aws:iam::123456789012:role/my-analytics-role'
},
Resource: {
Table: {
DatabaseName: databaseName,
Name: tableName,
},
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [], // empty = cannot re-grant to others
};
await lakeFormation.send(new GrantPermissionsCommand(input));
}
// Grant CREATE_TABLE on a database
export async function grantDatabaseCreateTable(
principalArn: string,
databaseName: string,
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: { Database: { Name: databaseName } },
Permissions: ['CREATE_TABLE', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
}
// Grant SELECT on ALL tables in a database (wildcard)
export async function grantAllTablesSelect(
principalArn: string,
databaseName: string,
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
Table: {
DatabaseName: databaseName,
TableWildcard: {}, // matches all tables in this database
},
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
}
The Principal.DataLakePrincipalIdentifier must be an IAM ARN: an IAM role ARN (arn:aws:iam::123456789012:role/name), an IAM user ARN, or the special value IAM_ALLOWED_PRINCIPALS which grants access to all IAM principals that have the corresponding IAM permissions (this is the legacy fallback mode and is not recommended for new setups).
Column-level security with TableWithColumns
To restrict which columns a principal can query, use the TableWithColumns resource type instead of Table. You can specify an explicit ColumnNames allowlist or a ColumnWildcard with an ExcludedColumnNames denylist. Only SELECT and DESCRIBE permissions are supported on TableWithColumns — you cannot grant INSERT or DELETE at the column level.
import { GrantPermissionsCommand } from '@aws-sdk/client-lakeformation';
// Grant SELECT on only specific columns (allowlist)
export async function grantColumnSubset(
principalArn: string,
databaseName: string,
tableName: string,
allowedColumns: string[],
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
TableWithColumns: {
DatabaseName: databaseName,
Name: tableName,
ColumnNames: allowedColumns,
// e.g. ['user_id', 'event_type', 'timestamp']
// The principal can only SELECT these columns; querying
// other columns raises AccessDeniedException in Athena/Glue
},
},
Permissions: ['SELECT'],
PermissionsWithGrantOption: [],
}));
}
// Grant SELECT on all columns EXCEPT sensitive ones (denylist — usually simpler)
export async function grantAllColumnsExcept(
principalArn: string,
databaseName: string,
tableName: string,
excludedColumns: string[],
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
TableWithColumns: {
DatabaseName: databaseName,
Name: tableName,
ColumnWildcard: {
ExcludedColumnNames: excludedColumns,
// e.g. ['ssn', 'credit_card', 'email']
},
},
},
Permissions: ['SELECT'],
PermissionsWithGrantOption: [],
}));
}
Column-level grants are additive with table-level grants. If a principal has a table-level SELECT grant and a TableWithColumns grant for specific columns, the table-level grant wins and they can see all columns. To enforce column restriction, ensure the principal has only a TableWithColumns grant, not a broad Table grant.
Granting data location access
The DataLocation resource type governs which principals can register new S3 paths with Lake Formation (via RegisterResourceCommand). This is distinct from the read/write permissions on data stored at that path.
import { GrantPermissionsCommand } from '@aws-sdk/client-lakeformation';
export async function grantDataLocationAccess(
principalArn: string,
s3LocationArn: string, // e.g. 'arn:aws:s3:::my-bucket/datalake/'
): Promise<void> {
await lakeFormation.send(new GrantPermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
DataLocation: {
ResourceArn: s3LocationArn,
},
},
Permissions: ['DATA_LOCATION_ACCESS'],
PermissionsWithGrantOption: [],
}));
}
Batch granting permissions
BatchGrantPermissionsCommand issues up to 20 permission grants in a single API call. Each entry has an Id (a client-side string for correlating errors in the response — not stored by Lake Formation), a Principal, a Resource, and Permissions. Failed entries are returned in the Failures array — the call does not throw on partial failure.
import {
BatchGrantPermissionsCommand,
type BatchPermissionsRequestEntry,
} from '@aws-sdk/client-lakeformation';
export async function grantAnalyticsRolePermissions(
roleArn: string,
databaseName: string,
): Promise<void> {
const entries: BatchPermissionsRequestEntry[] = [
{
Id: 'db-describe',
Principal: { DataLakePrincipalIdentifier: roleArn },
Resource: { Database: { Name: databaseName } },
Permissions: ['DESCRIBE'],
PermissionsWithGrantOption: [],
},
{
Id: 'events-select',
Principal: { DataLakePrincipalIdentifier: roleArn },
Resource: { Table: { DatabaseName: databaseName, Name: 'events' } },
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
},
{
Id: 'users-columns',
Principal: { DataLakePrincipalIdentifier: roleArn },
Resource: {
TableWithColumns: {
DatabaseName: databaseName,
Name: 'users',
ColumnWildcard: { ExcludedColumnNames: ['ssn', 'dob'] },
},
},
Permissions: ['SELECT'],
PermissionsWithGrantOption: [],
},
];
const response = await lakeFormation.send(
new BatchGrantPermissionsCommand({ Entries: entries })
);
if (response.Failures && response.Failures.length > 0) {
for (const failure of response.Failures) {
console.error(`Grant ${failure.RequestEntry?.Id} failed:`, failure.Error);
}
throw new Error(`${response.Failures.length} permission grants failed`);
}
}
Revoking permissions
Use RevokePermissionsCommand or BatchRevokePermissionsCommand to remove previously granted permissions. The Permissions and PermissionsWithGrantOption arrays must exactly match what was granted — revoking SELECT does not automatically revoke SELECT with grant option; revoke both explicitly if you granted both.
import {
RevokePermissionsCommand,
BatchRevokePermissionsCommand,
} from '@aws-sdk/client-lakeformation';
export async function revokeTableSelect(
principalArn: string,
databaseName: string,
tableName: string,
): Promise<void> {
await lakeFormation.send(new RevokePermissionsCommand({
Principal: { DataLakePrincipalIdentifier: principalArn },
Resource: {
Table: { DatabaseName: databaseName, Name: tableName },
},
Permissions: ['SELECT', 'DESCRIBE'],
PermissionsWithGrantOption: [],
}));
// Note: if the principal also has PermissionsWithGrantOption: ['SELECT'],
// you must issue a separate revoke with PermissionsWithGrantOption: ['SELECT']
}
Revoking a permission that was never granted returns InvalidInputException with the message "Grantee has no such permission". To safely revoke, use ListPermissionsCommand first to confirm the grant exists.
Listing permissions with ListPermissionsCommand
ListPermissionsCommand returns all Lake Formation permission grants matching optional filters for resource type, resource, and principal. Use it to audit what access an IAM role has, or to find all principals with access to a specific table.
import {
ListPermissionsCommand,
type ListPermissionsCommandInput,
} from '@aws-sdk/client-lakeformation';
// List all permissions for a specific IAM role
export async function listRolePermissions(roleArn: string) {
const grants = [];
let nextToken: string | undefined;
do {
const input: ListPermissionsCommandInput = {
Principal: { DataLakePrincipalIdentifier: roleArn },
NextToken: nextToken,
};
const response = await lakeFormation.send(
new ListPermissionsCommand(input)
);
grants.push(...(response.PrincipalResourcePermissions ?? []));
nextToken = response.NextToken;
} while (nextToken);
return grants;
}
// List all principals with SELECT on a specific table
export async function listTablePermissions(
databaseName: string,
tableName: string,
) {
const grants = [];
let nextToken: string | undefined;
do {
const response = await lakeFormation.send(new ListPermissionsCommand({
Resource: {
Table: { DatabaseName: databaseName, Name: tableName },
},
NextToken: nextToken,
}));
grants.push(...(response.PrincipalResourcePermissions ?? []));
nextToken = response.NextToken;
} while (nextToken);
// Filter to only SELECT grants
return grants.filter(g => g.Permissions?.includes('SELECT'));
}
The ResourceType filter parameter accepts values like 'TABLE', 'DATABASE', 'DATA_LOCATION' to narrow results without specifying exact resource identifiers. This is useful for listing all table-level grants across a database without enumerating every table name.
Error handling reference
| Error | Cause | Fix |
|---|---|---|
InvalidInputException | Permission not valid for resource type, or revoking a non-existent grant | Check resource type / permission compatibility table above; audit with ListPermissionsCommand before revoking |
EntityNotFoundException | Database or table not found in Glue Data Catalog | Verify database/table names with glue:GetDatabase / glue:GetTable |
AccessDeniedException | Caller not a data lake admin and lacks lakeformation:GrantPermissions | Either add caller as a data lake admin via PutDataLakeSettings, or add IAM lakeformation:GrantPermissions |
ConcurrentModificationException | Concurrent grant/revoke on the same resource | Retry with exponential backoff — Lake Formation uses optimistic concurrency on permission records |
ColumnStatisticsError | TableWithColumns references a column that does not exist in the Glue schema | Verify column names against glue:GetTable response before granting |
Related pages
- Lake Formation overview — RegisterResourceCommand, data lake settings — registering S3 resources and configuring data lake admins
- Lake Formation LF-tags — TBAC, CreateLFTagCommand, tag expressions — tag-based access control for scalable permission management
- Lake Formation governed tables — StartTransactionCommand, UpdateTableObjects — ACID transactions on data lake objects
- AWS Glue Data Catalog for MCP servers
- Athena workgroups for MCP servers