Skip to content

Commit ecfaf9d

Browse files
committed
CASSSIDECAR-346 Sidecar side of CEP-55
1 parent 7ac3ac8 commit ecfaf9d

15 files changed

Lines changed: 875 additions & 0 deletions

File tree

adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraAdapter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.cassandra.sidecar.common.server.ICassandraAdapter;
3535
import org.apache.cassandra.sidecar.common.server.JmxClient;
3636
import org.apache.cassandra.sidecar.common.server.MetricsOperations;
37+
import org.apache.cassandra.sidecar.common.server.RolesOperations;
3738
import org.apache.cassandra.sidecar.common.server.StorageOperations;
3839
import org.apache.cassandra.sidecar.common.server.TableOperations;
3940
import org.apache.cassandra.sidecar.common.server.dns.DnsResolver;
@@ -136,6 +137,13 @@ public MetricsOperations metricsOperations()
136137
return new CassandraMetricsOperations(jmxClient, tableSchemaFetcher, this);
137138
}
138139

140+
@Override
141+
@NotNull
142+
public RolesOperations rolesOperations()
143+
{
144+
return new CassandraRolesOperations(this);
145+
}
146+
139147
/**
140148
* {@inheritDoc}
141149
*/
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.sidecar.adapters.base;
20+
21+
import java.util.Iterator;
22+
import java.util.List;
23+
import java.util.Locale;
24+
import java.util.Map;
25+
import java.util.Random;
26+
import java.util.UUID;
27+
import java.util.regex.Pattern;
28+
29+
import com.google.common.annotations.VisibleForTesting;
30+
31+
import com.datastax.driver.core.ExecutionInfo;
32+
import com.datastax.driver.core.ResultSet;
33+
import com.datastax.driver.core.Row;
34+
import com.datastax.driver.core.SimpleStatement;
35+
import org.apache.cassandra.sidecar.common.request.data.GenerateRoleRequestPayload;
36+
import org.apache.cassandra.sidecar.common.response.GenerateRoleResponse;
37+
import org.apache.cassandra.sidecar.common.server.ICassandraAdapter;
38+
import org.apache.cassandra.sidecar.common.server.RolesOperations;
39+
40+
public class CassandraRolesOperations implements RolesOperations
41+
{
42+
private final UUIDGenerator generator;
43+
private final ICassandraAdapter cassandraAdapter;
44+
45+
public CassandraRolesOperations(ICassandraAdapter cassandraAdapter)
46+
{
47+
this.cassandraAdapter = cassandraAdapter;
48+
this.generator = new UUIDGenerator();
49+
}
50+
51+
public GenerateRoleResponse generateRole(GenerateRoleRequestPayload payload)
52+
{
53+
String role = null;
54+
String password = null;
55+
if (payload.sidecarGeneration)
56+
{
57+
role = generator.generate(payload.roleNameOptions);
58+
// we do not accept any parameters for password, it will be just uuid
59+
if (payload.passwordGeneration)
60+
password = generator.generate(Map.of());
61+
}
62+
63+
try
64+
{
65+
SimpleStatement statement = new SimpleStatement(constructQuery(payload, role, password));
66+
ResultSet resultSet = cassandraAdapter.executeLocal(statement);
67+
68+
ExecutionInfo executionInfo = resultSet.getExecutionInfo();
69+
List<String> warnings = executionInfo.getWarnings();
70+
if (warnings != null && !warnings.isEmpty())
71+
throw new IllegalStateException(String.join(" ", warnings));
72+
73+
if (payload.sidecarGeneration)
74+
{
75+
return new GenerateRoleResponse(role, password);
76+
}
77+
else
78+
{
79+
Row one = resultSet.one();
80+
return new GenerateRoleResponse(one.getString("generated_role"), one.getString("generated_password"));
81+
}
82+
}
83+
catch (Throwable t)
84+
{
85+
throw new IllegalStateException(t);
86+
}
87+
}
88+
89+
private String constructQuery(GenerateRoleRequestPayload payload, String generatedRole, String generatedPassword)
90+
{
91+
String query;
92+
boolean withStarted = false;
93+
if (payload.sidecarGeneration)
94+
{
95+
if (payload.passwordGeneration)
96+
{
97+
query = "CREATE ROLE " + generatedRole + " WITH PASSWORD = '" + generatedPassword + "'";
98+
withStarted = true;
99+
}
100+
else
101+
{
102+
query = "CREATE ROLE " + generatedRole;
103+
withStarted = true;
104+
}
105+
}
106+
else
107+
{
108+
if (payload.passwordGeneration)
109+
{
110+
query = "CREATE GENERATED ROLE WITH GENERATED PASSWORD";
111+
withStarted = true;
112+
}
113+
else
114+
{
115+
query = "CREATE GENERATED ROLE";
116+
}
117+
}
118+
119+
if (!payload.sidecarGeneration)
120+
{
121+
122+
StringBuilder sb = new StringBuilder();
123+
Map<String, String> config = payload.roleNameOptions();
124+
if (config != null)
125+
{
126+
Iterator<Map.Entry<String, String>> iterator = config.entrySet().iterator();
127+
while (iterator.hasNext())
128+
{
129+
Map.Entry<String, String> next = iterator.next();
130+
131+
sb.append("'").append(next.getKey()).append("'")
132+
.append(":")
133+
.append("'").append(next.getValue()).append("'");
134+
135+
if (iterator.hasNext())
136+
sb.append(",");
137+
}
138+
}
139+
140+
String maybeOptions = sb.toString();
141+
if (!maybeOptions.isBlank())
142+
{
143+
if (withStarted)
144+
query = query + " AND OPTIONS = {" + maybeOptions + "}";
145+
else
146+
query = query + " WITH OPTIONS = {" + maybeOptions + "}";
147+
}
148+
}
149+
150+
return query;
151+
}
152+
153+
/**
154+
* Generator of UUIDs where first character is always a character.
155+
*/
156+
@VisibleForTesting
157+
public static class UUIDGenerator
158+
{
159+
public static final String NAME_PREFIX_KEY = "name_prefix";
160+
public static final String NAME_SUFFIX_KEY = "name_suffix";
161+
public static final String NAME_SIZE = "name_size";
162+
public static final int MINIMUM_NAME_SIZE = 10;
163+
164+
private static final Pattern PATTERN = Pattern.compile("-");
165+
public static final char[] FIRST_CHARS = { 'a', 'b', 'c', 'd', 'e', 'f' };
166+
167+
private static final Random random = new Random();
168+
// lenght of UUID without hyphens
169+
// generated name can be longer than this if it has prefix / suffix
170+
public static final int MAXIMUM_NAME_SIZE = 32;
171+
172+
public String generate(Map<String, String> options)
173+
{
174+
int size = getSize(options);
175+
176+
// to always start on a letter, so we do not need to wrap in ''
177+
char firstChar = FIRST_CHARS[random.nextInt(6)];
178+
String uuid = UUID.randomUUID().toString().toLowerCase(Locale.ROOT);
179+
String uuidWithoutHyphens = PATTERN.matcher(uuid).replaceAll("");
180+
String name = firstChar + uuidWithoutHyphens.substring(1);
181+
182+
name = name.substring(0, size);
183+
name = enrich(NAME_PREFIX_KEY, name, options);
184+
name = enrich(NAME_SUFFIX_KEY, name, options);
185+
186+
return name;
187+
}
188+
189+
private String enrich(String key, String generatedValue, Map<String, String> options)
190+
{
191+
if (options == null || options.isEmpty())
192+
return generatedValue;
193+
194+
if (options.containsKey(key))
195+
{
196+
Object value = options.get(key);
197+
198+
if (value == null)
199+
throw new IllegalArgumentException("Value of " + key + " cannot be null.");
200+
201+
if (NAME_PREFIX_KEY.equals(key))
202+
generatedValue = value + generatedValue;
203+
else if (NAME_SUFFIX_KEY.equals(key))
204+
generatedValue = generatedValue + value;
205+
}
206+
207+
return generatedValue;
208+
}
209+
210+
private int getSize(Map<String, String> options)
211+
{
212+
Object sizeObject;
213+
if (options == null)
214+
{
215+
sizeObject = MAXIMUM_NAME_SIZE;
216+
}
217+
else if (options.containsKey(NAME_SIZE))
218+
{
219+
Object nameSizeValue = options.get(NAME_SIZE);
220+
if (nameSizeValue != null)
221+
sizeObject = nameSizeValue;
222+
else
223+
throw new IllegalArgumentException("Value of " + NAME_SIZE + " has to be strictly positive integer.");
224+
}
225+
else
226+
{
227+
sizeObject = MAXIMUM_NAME_SIZE;
228+
}
229+
230+
int size;
231+
232+
if (sizeObject instanceof String)
233+
{
234+
try
235+
{
236+
size = Integer.parseInt((String) sizeObject);
237+
}
238+
catch (Throwable t)
239+
{
240+
throw new IllegalArgumentException("Value '" + sizeObject + "' can't be converted to integer.");
241+
}
242+
}
243+
else size = ((Number) sizeObject).intValue();
244+
245+
if (size < MINIMUM_NAME_SIZE)
246+
throw new IllegalArgumentException("Value of " + NAME_SIZE + " parameter has to be at least " + MINIMUM_NAME_SIZE + '.');
247+
248+
if (size > MAXIMUM_NAME_SIZE)
249+
throw new IllegalArgumentException("Generator generates names of maximum length " + MAXIMUM_NAME_SIZE + ". " +
250+
"You want to generate with length " + size + '.');
251+
252+
return size;
253+
}
254+
}
255+
}

client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ public final class ApiEndpointsV1
161161
public static final String LIVE_MIGRATION_DATA_COPY_TASKS_ROUTE = LIVE_MIGRATION_API_PREFIX + "/data-copy-tasks";
162162
public static final String LIVE_MIGRATION_DATA_COPY_TASK_ROUTE = LIVE_MIGRATION_DATA_COPY_TASKS_ROUTE + "/:taskId";
163163

164+
public static final String GENERATE_ROLE = API_V1 + CASSANDRA + "/generate-role";
165+
164166
public static final String OPENAPI_JSON_ROUTE = "/spec/openapi.json";
165167
public static final String OPENAPI_YAML_ROUTE = "/spec/openapi.yaml";
166168
// With the wildcard, the index.html page under resources/docs/openapi/index.html will render
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.sidecar.common.request;
20+
21+
import io.netty.handler.codec.http.HttpMethod;
22+
import org.apache.cassandra.sidecar.common.request.data.GenerateRoleRequestPayload;
23+
import org.apache.cassandra.sidecar.common.response.GenerateRoleResponse;
24+
25+
import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.GENERATE_ROLE;
26+
27+
/**
28+
* Server-side role generation needs CEP-55. If executed against older nodes,
29+
* then client-side generation has to be used.
30+
*/
31+
public class GenerateRoleRequest extends JsonRequest<GenerateRoleResponse>
32+
{
33+
public static final String GENERATE_PASSWORD_PARAM = "generate_password";
34+
public static final String SIDECAR_GENERATION_PARAM = "sidecar_generation";
35+
36+
private final GenerateRoleRequestPayload payload;
37+
38+
/**
39+
* Constructs a request to generate a role.
40+
*
41+
* @param payload payload with generation parameters
42+
*/
43+
public GenerateRoleRequest(GenerateRoleRequestPayload payload)
44+
{
45+
super(GENERATE_ROLE);
46+
this.payload = payload;
47+
}
48+
49+
public HttpMethod method()
50+
{
51+
return HttpMethod.PUT;
52+
}
53+
54+
public GenerateRoleRequestPayload requestBody()
55+
{
56+
return payload;
57+
}
58+
}

0 commit comments

Comments
 (0)