Skip to content

Commit d6be728

Browse files
committed
object storage: give each bucket its own credential with two rotatable key slots
1 parent dc0ba1c commit d6be728

61 files changed

Lines changed: 5234 additions & 51 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

PendingReleaseNotes

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,34 @@ example.ver.1 > example.ver.2:
5353
the guest while the NVRAM sidecar is copied, so that the captured firmware
5454
state is consistent with the disk snapshot. Non-UEFI VMs are unaffected and
5555
continue to snapshot live.
56+
57+
4.22.1.0 > 24.0.0.0:
58+
* Per-bucket credentials for object storage. Buckets on Ceph RGW object storage
59+
now each get their own credential, and each credential has two key slots that
60+
rotate independently, so one key can be replaced while consumers keep working
61+
on the other. Every bucket in an account previously shared one credential, so
62+
a leaked key exposed all of them and there was no way to rotate it.
63+
64+
- Requires Ceph Squid (v19) or later, because it is built on RGW accounts.
65+
Object storage that cannot provide per-bucket credentials keeps the existing
66+
per-account behaviour, and says so in the UI.
67+
68+
- Existing accounts keep the shared credential until an administrator migrates
69+
them, from the Object Storage tab on the account. Migration is per account
70+
per object store, and cannot be undone: it adopts the account's existing RGW
71+
user as the root of a new RGW account at the gateway. Buckets created before
72+
the migration keep working on the shared key until each one is given its own
73+
credential.
74+
75+
- Once every bucket on an object store has its own credential, an administrator
76+
can rotate the account's own key, so that the key that was shared with users
77+
no longer works.
78+
79+
- The global setting object.storage.per.bucket.credentials decides how an
80+
account is set up the first time it uses an object store. Turning it off
81+
leaves already-migrated accounts as they are.
82+
83+
* listBuckets now applies its objectstorageid parameter. The parameter has been
84+
accepted since 4.19.0 but never filtered, so any caller passing it received
85+
every bucket. Those callers will now receive only the buckets on that object
86+
store.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.agent.api.to;
18+
19+
import java.util.List;
20+
21+
/**
22+
* A dedicated backend identity provisioned for a single bucket by an object
23+
* store provider, together with the key pairs it currently holds.
24+
*/
25+
public final class BucketCredentialTO {
26+
27+
private final String providerCredentialId;
28+
29+
private final List<BucketKeyTO> keys;
30+
31+
public BucketCredentialTO(String providerCredentialId, List<BucketKeyTO> keys) {
32+
this.providerCredentialId = providerCredentialId;
33+
this.keys = keys;
34+
}
35+
36+
public String getProviderCredentialId() {
37+
return providerCredentialId;
38+
}
39+
40+
public List<BucketKeyTO> getKeys() {
41+
return keys;
42+
}
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.agent.api.to;
18+
19+
/**
20+
* A single access/secret key pair of a bucket credential, as returned by an
21+
* object store provider.
22+
*/
23+
public final class BucketKeyTO {
24+
25+
private final String accessKey;
26+
27+
private final String secretKey;
28+
29+
public BucketKeyTO(String accessKey, String secretKey) {
30+
this.accessKey = accessKey;
31+
this.secretKey = secretKey;
32+
}
33+
34+
public String getAccessKey() {
35+
return accessKey;
36+
}
37+
38+
public String getSecretKey() {
39+
return secretKey;
40+
}
41+
}

api/src/main/java/com/cloud/agent/api/to/BucketTO.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@ public final class BucketTO {
2222

2323
private String name;
2424

25+
private String uuid;
26+
2527
private String accessKey;
2628

2729
private String secretKey;
2830

2931
private long accountId;
3032

33+
private String providerCredentialId;
34+
3135
public BucketTO(Bucket bucket) {
3236
this.name = bucket.getName();
37+
this.uuid = bucket.getUuid();
3338
this.accessKey = bucket.getAccessKey();
3439
this.secretKey = bucket.getSecretKey();
3540
this.accountId = bucket.getAccountId();
@@ -43,6 +48,10 @@ public String getName() {
4348
return this.name;
4449
}
4550

51+
public String getUuid() {
52+
return this.uuid;
53+
}
54+
4655
public String getAccessKey() {
4756
return this.accessKey;
4857
}
@@ -54,4 +63,12 @@ public String getSecretKey() {
5463
public long getAccountId() {
5564
return this.accountId;
5665
}
66+
67+
public String getProviderCredentialId() {
68+
return this.providerCredentialId;
69+
}
70+
71+
public void setProviderCredentialId(String providerCredentialId) {
72+
this.providerCredentialId = providerCredentialId;
73+
}
5774
}

api/src/main/java/com/cloud/event/EventTypes.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,11 @@ public class EventTypes {
816816
public static final String EVENT_BUCKET_CREATE = "BUCKET.CREATE";
817817
public static final String EVENT_BUCKET_DELETE = "BUCKET.DELETE";
818818
public static final String EVENT_BUCKET_UPDATE = "BUCKET.UPDATE";
819+
public static final String EVENT_BUCKET_KEY_ROTATE = "BUCKET.KEY.ROTATE";
820+
public static final String EVENT_BUCKET_KEY_REVOKE = "BUCKET.KEY.REVOKE";
821+
public static final String EVENT_BUCKET_CREDENTIAL_MIGRATE = "BUCKET.CREDENTIAL.MIGRATE";
822+
public static final String EVENT_OBJECT_STORE_ACCOUNT_MIGRATE = "OBJECTSTORE.ACCOUNT.MIGRATE";
823+
public static final String EVENT_OBJECT_STORE_ACCOUNT_KEY_ROTATE = "OBJECTSTORE.ACCOUNT.KEY.ROTATE";
819824

820825
// Quota
821826
public static final String EVENT_QUOTA_TARIFF_CREATE = "QUOTA.TARIFF.CREATE";
@@ -1395,6 +1400,11 @@ public class EventTypes {
13951400
entityEventDetails.put(EVENT_BUCKET_CREATE, Bucket.class);
13961401
entityEventDetails.put(EVENT_BUCKET_UPDATE, Bucket.class);
13971402
entityEventDetails.put(EVENT_BUCKET_DELETE, Bucket.class);
1403+
entityEventDetails.put(EVENT_BUCKET_KEY_ROTATE, Bucket.class);
1404+
entityEventDetails.put(EVENT_BUCKET_KEY_REVOKE, Bucket.class);
1405+
entityEventDetails.put(EVENT_BUCKET_CREDENTIAL_MIGRATE, Bucket.class);
1406+
entityEventDetails.put(EVENT_OBJECT_STORE_ACCOUNT_MIGRATE, Account.class);
1407+
entityEventDetails.put(EVENT_OBJECT_STORE_ACCOUNT_KEY_ROTATE, Account.class);
13981408

13991409
// Quota
14001410
entityEventDetails.put(EVENT_QUOTA_TARIFF_CREATE, QuotaTariff.class);

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,10 @@ public class ApiConstants {
13821382
public static final String BUCKET_LIMIT = "bucketlimit";
13831383
public static final String BUCKET_TOTAL = "buckettotal";
13841384
public static final String OBJECT_STORAGE_ID = "objectstorageid";
1385+
public static final String KEY_SLOT = "keyslot";
1386+
public static final String LAST_USED = "lastused";
1387+
public static final String CREDENTIAL_SCOPE = "credentialscope";
1388+
public static final String CREDENTIAL_KEYS = "keys";
13851389
public static final String OBJECT_STORAGE = "objectstore";
13861390
public static final String OBJECT_STORAGE_AVAILABLE = "objectstorageavailable";
13871391
public static final String OBJECT_STORAGE_LIMIT = "objectstoragelimit";

api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.cloudstack.api.response.BackupRepositoryResponse;
4545
import org.apache.cloudstack.api.response.BackupScheduleResponse;
4646
import org.apache.cloudstack.api.response.BaseRolePermissionResponse;
47+
import org.apache.cloudstack.api.response.BucketKeyResponse;
4748
import org.apache.cloudstack.api.response.BucketResponse;
4849
import org.apache.cloudstack.api.response.CapacityResponse;
4950
import org.apache.cloudstack.api.response.ClusterResponse;
@@ -163,6 +164,7 @@
163164
import org.apache.cloudstack.region.Region;
164165
import org.apache.cloudstack.secstorage.heuristics.Heuristic;
165166
import org.apache.cloudstack.storage.object.Bucket;
167+
import org.apache.cloudstack.storage.object.BucketCredentialKey;
166168
import org.apache.cloudstack.storage.object.ObjectStore;
167169
import org.apache.cloudstack.storage.sharedfs.SharedFS;
168170
import org.apache.cloudstack.usage.Usage;
@@ -592,6 +594,8 @@ DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvi
592594

593595
BucketResponse createBucketResponse(Bucket bucket);
594596

597+
BucketKeyResponse createBucketKeyResponse(BucketCredentialKey key);
598+
595599
ASNRangeResponse createASNumberRangeResponse(ASNumberRange asnRange);
596600

597601
ASNumberResponse createASNumberResponse(ASNumber asn);

api/src/main/java/org/apache/cloudstack/api/command/admin/storage/ListObjectStoragePoolsCmd.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.cloudstack.api.BaseListCmd;
2323
import org.apache.cloudstack.api.Parameter;
2424
import org.apache.cloudstack.api.response.ListResponse;
25+
import org.apache.cloudstack.api.response.AccountResponse;
2526
import org.apache.cloudstack.api.response.ObjectStoreResponse;
2627

2728
@APICommand(name = "listObjectStoragePools", description = "Lists object storage pools.", responseObject = ObjectStoreResponse.class, since = "4.19.0",
@@ -43,6 +44,10 @@ public class ListObjectStoragePoolsCmd extends BaseListCmd {
4344
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ObjectStoreResponse.class, description = "the ID of the storage pool")
4445
private Long id;
4546

47+
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
48+
description = "when given, each store also reports the credential state of this account on it: accountcredentialscope and legacybuckets", since = "24.0.0")
49+
private Long accountId;
50+
4651
/////////////////////////////////////////////////////
4752
/////////////////// Accessors ///////////////////////
4853
/////////////////////////////////////////////////////
@@ -56,6 +61,10 @@ public Long getId() {
5661
return id;
5762
}
5863

64+
public Long getAccountId() {
65+
return accountId;
66+
}
67+
5968
public String getProvider() {
6069
return provider;
6170
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.storage;
18+
19+
import com.cloud.exception.InvalidParameterValueException;
20+
import com.cloud.user.Account;
21+
import org.apache.cloudstack.acl.RoleType;
22+
import org.apache.cloudstack.api.APICommand;
23+
import org.apache.cloudstack.api.ApiCommandResourceType;
24+
import org.apache.cloudstack.api.ApiConstants;
25+
import org.apache.cloudstack.api.ApiErrorCode;
26+
import org.apache.cloudstack.api.BaseCmd;
27+
import org.apache.cloudstack.api.Parameter;
28+
import org.apache.cloudstack.api.ServerApiException;
29+
import org.apache.cloudstack.api.response.AccountResponse;
30+
import org.apache.cloudstack.api.response.ObjectStoreResponse;
31+
import org.apache.cloudstack.api.response.SuccessResponse;
32+
import org.apache.cloudstack.context.CallContext;
33+
34+
@APICommand(name = "migrateObjectStoreAccount", description = "Migrates an account's identity on an object store into the state its provider requires for per-bucket credentials. On Ceph RGW this creates an RGW account and adopts the account's existing RGW user into it as the account root, which transfers ownership of all its buckets to the RGW account and is permanent. Existing buckets keep working with their current keys; they gain dedicated credentials individually via migrateBucketCredential.",
35+
responseObject = SuccessResponse.class, entityType = {Account.class},
36+
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "24.0.0",
37+
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin})
38+
public class MigrateObjectStoreAccountCmd extends BaseCmd {
39+
40+
/////////////////////////////////////////////////////
41+
//////////////// API parameters /////////////////////
42+
/////////////////////////////////////////////////////
43+
44+
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
45+
required = true, description = "The ID of the account to migrate")
46+
private Long accountId;
47+
48+
@Parameter(name = ApiConstants.OBJECT_STORAGE_ID, type = CommandType.UUID, entityType = ObjectStoreResponse.class,
49+
required = true, description = "The ID of the object store on which to migrate the account")
50+
private Long objectStoreId;
51+
52+
/////////////////////////////////////////////////////
53+
/////////////////// Accessors ///////////////////////
54+
/////////////////////////////////////////////////////
55+
56+
public Long getAccountId() {
57+
return accountId;
58+
}
59+
60+
public Long getObjectStoreId() {
61+
return objectStoreId;
62+
}
63+
64+
/////////////////////////////////////////////////////
65+
/////////////// API Implementation///////////////////
66+
/////////////////////////////////////////////////////
67+
68+
@Override
69+
public long getEntityOwnerId() {
70+
Account account = _entityMgr.findById(Account.class, getAccountId());
71+
if (account != null) {
72+
return account.getId();
73+
}
74+
return Account.ACCOUNT_ID_SYSTEM;
75+
}
76+
77+
@Override
78+
public Long getApiResourceId() {
79+
return accountId;
80+
}
81+
82+
@Override
83+
public ApiCommandResourceType getApiResourceType() {
84+
return ApiCommandResourceType.Account;
85+
}
86+
87+
@Override
88+
public void execute() {
89+
CallContext.current().setEventDetails("Account ID: " + getResourceUuid(ApiConstants.ACCOUNT_ID) + " object store ID: " + getResourceUuid(ApiConstants.OBJECT_STORAGE_ID));
90+
boolean result;
91+
try {
92+
result = _bucketService.migrateObjectStoreAccount(this, CallContext.current().getCallingAccount());
93+
} catch (InvalidParameterValueException e) {
94+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, e.getMessage());
95+
} catch (Exception e) {
96+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Error while migrating account on object store. " + e.getMessage());
97+
}
98+
if (result) {
99+
setResponseObject(new SuccessResponse(getCommandName()));
100+
} else {
101+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to migrate account on object store");
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)