alternator: hide internal tags from users
The "tags" mechanism in Alternator is a convenient way to attach metadata
to Alternator tables. Recently we have started using it more and more for
internal metadata storage:
* UpdateTimeToLive stores the attribute in a tag system:ttl_attribute
* CreateTable stores provisioned throughput in tags
system:provisioned_rcu and system:provisioned_wcu
* CreateTable stores the table's creation time in a tag called
system:table_creation_time.
We do not want any of these internal tags to be visible to a
ListTagsOfResource request, because if they are visible (as before this
patch), systems such as Terraform can get confused when they suddenly
see a tag which they didn't set - and may even attempt to delete it
(as reported in issue #24098).
Moreover, we don't want any of these internal tags to be writable
with TagResource or UntagResource: If a user wants to change the TTL
setting they should do it via UpdateTimeToLive - not by writing
directly to tags.
So in this patch we forbid read or write to *any* tag that begins
with the "system:" prefix, except one: "system:write_isolation".
That tag is deliberately intended to be writable by the user, as
a configuration mechanism, and is never created internally by
Scylla. We should have perhaps chosen a different prefix for
configurable vs. internal tags, or chosen more unique prefixes -
but let's not change these historic names now.
This patch also adds regression tests for the internal tags features,
failing before this patch and passing after:
1. internal tags, specifically system:ttl_attribute, are not visible
in ListTagsOfResource, and cannot be modified by TagResource or
UntagResource.
2. system:write_isolation is not internal, and be written by either
TagResource or UntagResource, and read with ListTagsOfResource.
This patch also fixes a bug in the test where we added more checks
for system:write_isolation - test_tag_resource_write_isolation_values.
This test forgot to remove the system:write_isolation tags from
test_table when it ended, which would lead to other tests that run
later to run with a non-default write isolation - something which we
never intended.
Fixes #24098.
Signed-off-by: Nadav Har'El <nyh@scylladb.com>
Closes scylladb/scylladb#24299
This commit is contained in:
committed by
Botond Dénes
parent
37e6ff1a3c
commit
6cbcabd100
@@ -1034,6 +1034,17 @@ void rmw_operation::set_default_write_isolation(std::string_view value) {
|
||||
default_write_isolation = parse_write_isolation(value);
|
||||
}
|
||||
|
||||
// Alternator uses tags whose keys start with the "system:" prefix for
|
||||
// internal purposes. Those should not be readable by ListTagsOfResource,
|
||||
// nor writable with TagResource or UntagResource (see #24098).
|
||||
// Only a few specific system tags, currently only system:write_isolation,
|
||||
// are deliberately intended to be set and read by the user, so are not
|
||||
// considered "internal".
|
||||
static bool tag_key_is_internal(std::string_view tag_key) {
|
||||
return tag_key.starts_with("system:") &&
|
||||
tag_key != rmw_operation::WRITE_ISOLATION_TAG_KEY;
|
||||
}
|
||||
|
||||
enum class update_tags_action { add_tags, delete_tags };
|
||||
static void update_tags_map(const rjson::value& tags, std::map<sstring, sstring>& tags_map, update_tags_action action) {
|
||||
if (action == update_tags_action::add_tags) {
|
||||
@@ -1058,6 +1069,9 @@ static void update_tags_map(const rjson::value& tags, std::map<sstring, sstring>
|
||||
if (!validate_legal_tag_chars(tag_key)) {
|
||||
throw api_error::validation("A tag Key can only contain letters, spaces, and [+-=._:/]");
|
||||
}
|
||||
if (tag_key_is_internal(tag_key)) {
|
||||
throw api_error::validation(fmt::format("Tag key '{}' is reserved for internal use", tag_key));
|
||||
}
|
||||
// Note tag values are limited similarly to tag keys, but have a
|
||||
// longer length limit, and *can* be empty.
|
||||
if (tag_value.size() > 256) {
|
||||
@@ -1070,7 +1084,11 @@ static void update_tags_map(const rjson::value& tags, std::map<sstring, sstring>
|
||||
}
|
||||
} else if (action == update_tags_action::delete_tags) {
|
||||
for (auto it = tags.Begin(); it != tags.End(); ++it) {
|
||||
tags_map.erase(sstring(it->GetString(), it->GetStringLength()));
|
||||
auto tag_key = rjson::to_string_view(*it);
|
||||
if (tag_key_is_internal(tag_key)) {
|
||||
throw api_error::validation(fmt::format("Tag key '{}' is reserved for internal use", tag_key));
|
||||
}
|
||||
tags_map.erase(sstring(tag_key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1145,6 +1163,9 @@ future<executor::request_return_type> executor::list_tags_of_resource(client_sta
|
||||
|
||||
rjson::value& tags = ret["Tags"];
|
||||
for (auto& tag_entry : tags_map) {
|
||||
if (tag_key_is_internal(tag_entry.first)) {
|
||||
continue;
|
||||
}
|
||||
rjson::value new_entry = rjson::empty_object();
|
||||
rjson::add(new_entry, "Key", rjson::from_string(tag_entry.first));
|
||||
rjson::add(new_entry, "Value", rjson::from_string(tag_entry.second));
|
||||
|
||||
@@ -144,6 +144,13 @@ def test_tag_resource_write_isolation_values(scylla_only, test_table):
|
||||
test_table.meta.client.tag_resource(ResourceArn=arn, Tags=[{'Key':'system:write_isolation', 'Value':i}])
|
||||
with pytest.raises(ClientError, match='ValidationException'):
|
||||
test_table.meta.client.tag_resource(ResourceArn=arn, Tags=[{'Key':'system:write_isolation', 'Value':'bah'}])
|
||||
# Verify that reading system:write_isolation is possible (we didn't
|
||||
# accidentally prevent it while fixing #24098)
|
||||
keys = [tag['Key'] for tag in test_table.meta.client.list_tags_of_resource(ResourceArn=arn)['Tags']]
|
||||
assert 'system:write_isolation' in keys
|
||||
# Finally remove the system:write_isolation tag so to not modify the
|
||||
# default behavior of test_table.
|
||||
test_table.meta.client.untag_resource(ResourceArn=arn, TagKeys=['system:write_isolation'])
|
||||
|
||||
# Test that if trying to create a table with forbidden tags (in this test,
|
||||
# a list of tags longer than the maximum allowed of 50 tags), the table
|
||||
@@ -168,9 +175,9 @@ def test_too_long_tags_from_creation(dynamodb):
|
||||
dynamodb.meta.client.describe_table(TableName=name)
|
||||
|
||||
# This test is similar to the above, but uses another case of forbidden tags -
|
||||
# here an illegal value for the system::write_isolation tag. This is a
|
||||
# here an illegal value for the system:write_isolation tag. This is a
|
||||
# scylla_only test because only Alternator checks the validity of the
|
||||
# system::write_isolation tag.
|
||||
# system:write_isolation tag.
|
||||
# Reproduces issue #6809, where the table creation appeared to fail, but it
|
||||
# was actually created (without the tag).
|
||||
def test_forbidden_tags_from_creation(scylla_only, dynamodb):
|
||||
|
||||
@@ -13,8 +13,7 @@ from decimal import Decimal
|
||||
import pytest
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from test.alternator.util import new_test_table, random_string, full_query, unique_table_name, is_aws, \
|
||||
client_no_transform
|
||||
from .util import new_test_table, random_string, full_query, unique_table_name, is_aws, client_no_transform, multiset
|
||||
|
||||
# All tests in this file are expected to fail with tablets due to #16567.
|
||||
# To ensure that Alternator TTL is still being tested, instead of
|
||||
@@ -809,3 +808,42 @@ def test_ttl_expiration_long(dynamodb, waits_for_expiration):
|
||||
break
|
||||
time.sleep(max_duration/100.0)
|
||||
assert count == 99*N
|
||||
|
||||
# Alternator uses a tag "system:ttl_attribute" to store the TTL attribute
|
||||
# chosen by UpdateTimeToLive. However, this tag is not supposed to be
|
||||
# readable or writable by the user directly - it should be read or written
|
||||
# only with the usual UpdateTimeToLive and DescribeTimeToLive operations.
|
||||
# The following two test confirms that this is the case. The first test
|
||||
# checks that the internal tag is invisible, i.e., not returned by
|
||||
# ListTagsOfResource. Basically we check that enabling TTL does not add
|
||||
# any tags to the list of tags.
|
||||
# Reproduces issue #24098.
|
||||
def test_ttl_tag_is_invisible(dynamodb):
|
||||
with new_test_table(dynamodb,
|
||||
Tags=TAGS,
|
||||
KeySchema=[{ 'AttributeName': 'p', 'KeyType': 'HASH' }],
|
||||
AttributeDefinitions=[{ 'AttributeName': 'p', 'AttributeType': 'S' }]
|
||||
) as table:
|
||||
client = table.meta.client
|
||||
client.update_time_to_live(TableName=table.name,
|
||||
TimeToLiveSpecification={'AttributeName': 'x', 'Enabled': True})
|
||||
# Verify that TTL is set for this table, but no extra tags
|
||||
# like "system:ttl_attribute" (or anything else) are visible
|
||||
# in ListTagsOfResource:
|
||||
assert client.describe_time_to_live(TableName=table.name)['TimeToLiveDescription'] == {'TimeToLiveStatus': 'ENABLED', 'AttributeName': 'x'}
|
||||
arn = client.describe_table(TableName=table.name)['Table']['TableArn']
|
||||
assert multiset(TAGS) == multiset(client.list_tags_of_resource(ResourceArn=arn)['Tags'])
|
||||
|
||||
# Now check that the internal tag system:ttl_attribute cannot be written with
|
||||
# TagResource or UntagResource (it can only be modified by UpdateTimeToLive).
|
||||
# This is an Scylla-only test because in DynamoDB, there is nothing
|
||||
# special about the tag name "system:ttl_attribute", and it can be written.
|
||||
# Reproduces issue #24098.
|
||||
def test_ttl_tag_is_unwritable(test_table, scylla_only):
|
||||
tag_name = 'system:ttl_attribute'
|
||||
client = test_table.meta.client
|
||||
arn = client.describe_table(TableName=test_table.name)['Table']['TableArn']
|
||||
with pytest.raises(ClientError, match='ValidationException.*internal'):
|
||||
client.tag_resource(ResourceArn=arn, Tags=[{'Key': tag_name, 'Value': 'x'}])
|
||||
with pytest.raises(ClientError, match='ValidationException.*internal'):
|
||||
client.untag_resource(ResourceArn=arn, TagKeys=[tag_name])
|
||||
|
||||
Reference in New Issue
Block a user