Original Japanese article: Glue Data CatalogとS3TablesのそれぞれのIceberg REST APIの権限モデルを比べてみる
Introduction
I'm Aki, an AWS Community Builder (@jitepengin).
Across two previous articles, I hit the Iceberg REST Catalog directly and compared the design of the Glue endpoint and the S3 Tables endpoint:
- Hitting the Iceberg REST Catalog Directly: Understanding the Differences Between Glue Data Catalog and S3 Tables
- Verifying How IAM and Lake Formation Behave for the Glue REST Catalog and S3 Tables
Both of those took the angle of "looking at the same table through two different entry points." This time I wanted to try a different angle: laying the official per-operation permission mapping tables for both endpoints side by side.
For both endpoints, AWS publishes a table mapping each Iceberg REST operation to its IAM action, its Lake Formation permission, and its CloudTrail event name. Just placing these two tables next to each other turned up some interesting findings and design differences, which I've summarized below.
This time, rather than hands-on testing like before, this is a documentation-based comparison. Wherever something lines up with what I actually verified in the previous articles, I've noted that explicitly.
The official documentation I referenced:
- Glue side: https://docs.aws.amazon.com/glue/latest/dg/iceberg-rest-apis.html
- S3 Tables side: https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-integrating-open-source.html
Lining Up the Permission Mapping by Operation
| Operation | Glue: IAM action | Glue: LF permission | S3 Tables: IAM action |
|---|---|---|---|
| getConfig | glue:GetCatalog |
Not required | s3tables:GetTableBucket |
| listNamespaces | glue:GetDatabase |
ALL, DESCRIBE, SELECT | s3tables:ListNamespaces |
| createNamespace | glue:CreateDatabase |
ALL, DESCRIBE, SELECT | s3tables:CreateNamespace |
| loadNamespaceMetadata | glue:GetDatabase |
ALL, DESCRIBE, SELECT | s3tables:GetNamespace |
| updateNamespaceProperties | glue:UpdateDatabase |
ALL, ALTER | (not listed) |
| dropNamespace | glue:DeleteDatabase |
ALL, DROP | s3tables:DeleteNamespace |
| namespaceExists | (not listed) | - | s3tables:GetNamespace |
| listTables | glue:GetTables |
ALL, SELECT, DESCRIBE | s3tables:ListTables |
| createTable | glue:CreateTable |
ALL, CREATE_TABLE |
s3tables:CreateTable + s3tables:PutTableData
|
| loadTable | glue:GetTable |
ALL, SELECT, DESCRIBE |
s3tables:GetTableMetadataLocation + s3tables:GetTableData
|
| updateTable | glue:UpdateTable |
ALL, ALTER |
s3tables:UpdateTableMetadataLocation + s3tables:PutTableData + s3tables:GetTableData
|
| dropTable | glue:DeleteTable |
ALL, DROP | s3tables:DeleteTable |
| renameTable | (not listed; only possible via the extension API's UpdateTable) | - | s3tables:RenameTable |
| tableExists | glue:GetTable |
ALL, SELECT, DESCRIBE | s3tables:GetTable |
Even just laying these side by side surfaces several differences.
The Number of IAM Actions Differs for Data Operations
The first thing that jumps out is here. For operations that touch data, such as loadTable, createTable, and updateTable, the number of IAM actions required differs between the two.
The Glue endpoint's loadTable needs only one action, glue:GetTable, while the S3 Tables endpoint's loadTable needs two: s3tables:GetTableMetadataLocation and s3tables:GetTableData. createTable and updateTable show the same pattern: the S3 Tables side splits requirements into a "metadata operation action" and a "data body operation action."
This lines up with something I confirmed in a previous article: calling S3 Tables' LoadTable logs a GetTableMetadataLocation management event in CloudTrail.
What this suggests is that S3 Tables cleanly separates access to metadata from access to the data body at the API level, while the Glue endpoint consolidates everything into the single glue:GetTable action and delegates the actual storage access to Lake Formation's vended credentials. That, I think, is the design difference at play here.
Note
The
s3tables:CreateTable+s3tables:PutTableDatapair shown above is specifically the permission set for thecreateTableoperation as defined in the Iceberg REST API reference (s3-tables-integrating-open-source.html). A separate official document on SQL semantics (https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables-sql.html) states that theCreateTableAPI only creates an empty table inside the table bucket, and that setting the schema requires two more actions,UpdateTableMetadataLocationandGetTableMetadataLocation, for four in total.In other words, "the API call that creates the table" and "actually getting a usable table with a schema set" require different numbers of permissions. If you only look at the REST API's permission table and assume two actions are enough, you can get tripped up during schema setup.
The renameTable / namespaceExists Difference
The next difference: renameTable appears in S3 Tables' list of supported APIs but not in Glue's.
Glue's official documentation has no independent renameTable operation. Instead, a note on the AWS Glue extension API's (/extensions/v1/...) StartUpdateTableTransaction says: "A rename operation can also be performed through this API. In that case, the caller must also have glue:CreateTable or the Lake Formation CREATE_TABLE permission for the destination table." So on the Glue side, renaming isn't its own operation; it's treated as a kind of update transaction.
namespaceExists, meanwhile, exists as its own operation on the S3 Tables side (HEAD /v1/{prefix}/namespaces/{namespace}), but I couldn't find it anywhere in Glue's list of standard Iceberg REST Catalog API operations. Even though both implement the same Iceberg REST spec, the design decision of "what gets carved out as its own API" clearly differs.
The updateNamespaceProperties Difference
updateNamespaceProperties existing only on the Glue side is another difference worth calling out. Glue has a clearly defined updateNamespaceProperties operation with the IAM action glue:UpdateDatabase, but this operation doesn't exist at all in S3 Tables' list of operations. S3 Tables' documented limitations say "only the owner property is supported for namespaces," but there's no corresponding independent operation listed in the official operations table.
This might get added in a future update, but as things stand, it's another point of difference between the two.
The dropTable purge Behavior Difference
According to the official documentation, S3 Tables' dropTable returns a 400 Bad Request if you specify purge=false, meaning purge must be true. Glue's DeleteTable (for tables on S3), on the other hand, is documented as failing the operation and not deleting the data if you specify purge=true.
Same "delete the table" operation, but the required value of purge looks like it's flipped between the two according to the docs.
This looked like a likely source of mistakes, so I decided to actually check it hands-on. Testing it turned up something interesting.
I tried it first against the S3 Tables endpoint.
S3 Tables' purge Behavior
I created a table, then called dropTable with no parameters at all:
python -m awscurl --service s3tables --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/${BUCKET_ARN_PATH}/namespaces/analytics/tables/purge_test"
Result (excerpt):
{
"error": {
"code": 400,
"message": "DropTable operation failed. S3 Tables only supports dropping tables with purge enabled.",
"type": "purge_enabled"
}
}
As documented, this returned 400, and the table stayed in the listing.
Next, I re-ran it with the query parameter purge=true and got the exact same error. It turns out that, per the Iceberg REST Catalog spec, the query parameter for specifying purge on dropTable isn't purge; it's purgeRequested.
python -m awscurl --service s3tables --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://s3tables.ap-northeast-1.amazonaws.com/iceberg/v1/${BUCKET_ARN_PATH}/namespaces/analytics/tables/purge_test?purgeRequested=true"
This one succeeded with no response body, and I confirmed via the listing that the table had actually been deleted.
Note
The gist of "purge must be true" was correct per the documentation, but assuming the query parameter is named
purgewill trip you up. It's actuallypurgeRequested.
If you use both endpoints together, this alone is fertile ground for implementation mistakes.
Glue Data Catalog's purge Behavior
I then ran the same test on the Glue side.
My test environment has the Glue endpoint reaching S3 Tables-backed tables through the s3tablescatalog federated catalog. So I first called DeleteTable against this table via the Glue endpoint with purgeRequested=true specified, and it succeeded without any fuss.
python -m awscurl --service glue --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/purge_test_glue?purgeRequested=true"
This contradicts the official documentation's claim that "Glue's DeleteTable fails if purge=true." So I set up another table and tried deleting it via Glue without specifying purgeRequested at all.
python -m awscurl --service glue --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012:s3tablescatalog:penguin-rest-test/namespaces/analytics/tables/purge_test_glue2"
Result (excerpt):
{
"error": {
"code": 400,
"message": "PurgeRequested must be true for S3 federated iceberg tables.",
"type": "InvalidInputException"
}
}
The error message spells it out clearly: "S3 federated iceberg tables." So the Glue endpoint internally distinguishes whether a table's underlying entity is S3 Tables-backed (federated), and in that case it demands purgeRequested=true.
This tells us that the purge behavior of DeleteTable isn't determined by which endpoint you're hitting (Glue or S3 Tables). What determines it is the underlying entity of the table: whether the backend is an S3 Tables-federated table or a plain table on a general-purpose S3 bucket.
The documentation's statement that "Glue's DeleteTable fails for S3-backed tables if purge=true" is presumably describing a genuinely Glue-native Iceberg table on a general-purpose S3 bucket. For a table backed by S3 Tables, even when accessed through Glue, S3 Tables' own constraint (purgeRequested=true required) appears to pass straight through.
To test this hypothesis, I created an Iceberg table on a general-purpose S3 bucket via Athena and ran the same test through the Glue endpoint.
First, deleting without specifying purgeRequested succeeded:
python -m awscurl --service glue --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012/namespaces/general_iceberg_db/tables/purge_test_general"
Then, on a second table, specifying purgeRequested=true failed:
python -m awscurl --service glue --region ap-northeast-1 --profile your-profile \
-X DELETE \
"https://glue.ap-northeast-1.amazonaws.com/iceberg/v1/catalogs/123456789012/namespaces/general_iceberg_db/tables/purge_test_general2?purgeRequested=true"
Result (excerpt):
{
"error": {
"code": 400,
"message": "PurgeRequested cannot be true for Glue iceberg tables.",
"type": "InvalidInputException"
}
}
This mirrors the earlier error for the S3 Tables-backed table (PurgeRequested must be true for S3 federated iceberg tables.) almost perfectly. AWS itself clearly distinguishes "Glue iceberg tables" from "S3 federated iceberg tables" in its code, and imposes opposite purgeRequested requirements on each.
Summary of purge Behavior for Each
Here's the full result across all four cases tested:
| Table entity | Without purgeRequested
|
With purgeRequested=true
|
|---|---|---|
| S3 Tables-backed (via Glue endpoint) | 400 "PurgeRequested must be true for S3 federated iceberg tables" | Success |
| S3 Tables-backed (direct S3 Tables endpoint) | 400 "S3 Tables only supports dropping tables with purge enabled" | Success |
| General-purpose S3, Glue-native table (via Glue endpoint) | Success | 400 "PurgeRequested cannot be true for Glue iceberg tables" |
Both failure cases without purgeRequested return the same status code, 400. The nature of the error is consistent regardless of which endpoint you hit; the only difference is what the message says caused the failure.
It's not the entry point (Glue endpoint vs. S3 Tables endpoint) that decides the purgeRequested requirement; it's the underlying table entity (Glue Iceberg Tables vs. S3 Federated Iceberg Tables). Both the hands-on results and the error messages back this up.
I'd honestly prefer they picked one convention and stuck with it...
CloudTrail Granularity Also Reflects the API Design Split
S3 Tables' official documentation states clearly that "LoadTable generates a GetTableMetadataLocation management event and a GetTableData data event." This lines up with the IAM action split described above. S3 Tables appears to be designed to separate "metadata" from "the data body" at the audit-log level too.
On the Glue side, the official documentation shows each operation's CloudTrail event name matching its IAM action name (glue:GetTable logs as glue:GetTable), with no split like the one seen on S3 Tables. That said, as I confirmed hands-on in a previous article, when the Glue endpoint calls Lake Formation's GetDataAccess, that gets logged separately as its own event. It seems Glue's audit trail is split across two distinct lineages: "the log of the API operation itself" and "the Lake Formation authorization log."
Conclusion
This time, taking a different approach from the hands-on investigations I've done so far, I laid the official documentation-based permission mapping tables for Glue Data Catalog's and S3 Tables' respective Iceberg REST APIs side by side.
To summarize:
-
Granularity of data-operation permissions: Glue consolidates each operation into a single IAM action, while S3 Tables splits metadata and data-body actions apart (
createTableneeds only two actions as a REST operation, but per the SQL semantics documentation, actually getting a usable table with a schema set requires four). -
Rename operations: Glue has no independent API for this; it's handled as part of an update transaction. S3 Tables has its own dedicated
renameTableoperation. - namespaceExists: exists on the S3 Tables side but not in Glue's standard API list.
-
updateNamespaceProperties: exists on the Glue side but has no corresponding operation on S3 Tables (only the
ownerproperty is supported, in a limited way). -
dropTable's purge behavior: the query parameter is
purgeRequested, notpurge. Whether purge is required turns out to be determined not by "Glue endpoint vs. S3 Tables endpoint" but by the table's underlying entity (S3 Tables-federated vs. Glue-native on general-purpose S3), and the requirement is the exact opposite in each case, which I confirmed hands-on across all four scenarios. - CloudTrail granularity: S3 Tables logs separate events for metadata operations and data operations, while Glue logs one event per operation, with Lake Formation evaluation showing up as a separate lineage.
Even though both implement the same Iceberg REST Catalog spec, it turns out the granularity of the permission model and the design decisions around what gets carved out as an independent API differ quite a bit between Glue and S3 Tables. I think this is the same structural difference I touched on in earlier articles, Glue as a catalog hub and S3 Tables as a thin translation layer over storage, showing up consistently even at the level of permission mapping tables.
Comparing hands-on results against the documentation turns up things you'd miss looking at either one alone, so I'd like to keep doing this kind of exercise going forward.
I hope this article is useful to anyone choosing between the two endpoints.
Top comments (1)
The purgeRequested finding is the one that'll actually bite people: any wrapper that sets the flag per-endpoint instead of per-table-entity works fine until someone federates a bucket underneath it. Both failure modes come back as 400, so a client can't safely retry with the flag flipped without parsing the message string. Did you find anything in the loadTable response that lets you tell a Glue-native table from an S3-federated one before you attempt the drop?