Skip to content

Commit 30ab978

Browse files
authored
feat: improve users block and allow custom user ACLs (#14)
1 parent 25146a7 commit 30ab978

18 files changed

+471
-7
lines changed

codecov.yml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
coverage:
2+
status:
3+
patch:
4+
default:
5+
enabled: no

docs/specification.md

+52-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ The desired state file consists of:
99
- **Settings** [Optional]: Specific settings for configuring `kafka-gitops`.
1010
- **Topics** [Optional]: Topics and topic configuration definitions.
1111
- **Services** [Optional]: Service definitions for generating ACLs.
12+
- **Users** [Optional]: User definitions for generating ACLs.
1213
- **Custom Service ACLs** [Optional]: Definitions for custom, non-generated ACLs.
14+
- **Custom User ACLs** [Optional]: Definitions for custom, non-generated ACLs.
1315

1416
## Settings
1517

@@ -114,6 +116,31 @@ services:
114116

115117
Under the cover, `kafka-gitops` generates ACLs based on these definitions.
116118

119+
## Users
120+
121+
**Synopsis**: Define the users that will utilize your Kafka cluster. These user definitions allow `kafka-gitops` to generate ACLs for you. Yay!
122+
123+
?> **NOTE**: If using Confluent Cloud, users are service accounts that are prefixed with `user-`.
124+
125+
```yaml
126+
users:
127+
my-user:
128+
principal: User:my-user
129+
roles:
130+
- writer
131+
- reader
132+
- operator
133+
```
134+
135+
Currently, three predefined roles exist:
136+
137+
- **writer**: access to write to all topics
138+
- **reader**: access to read all topics using any consumer group
139+
- **operator**: access to view topics, topic configs, and to read topics and move their offsets
140+
141+
Outside of these very simple roles, you can define custom ACLs per-user by using the `customUserAcls` block.
142+
143+
117144
## Custom Service ACLs
118145

119146
**Synopsis**: Define custom ACLs for a specific service.
@@ -130,15 +157,38 @@ customServiceAcls:
130157
type: TOPIC
131158
pattern: PREFIXED
132159
host: "*"
133-
principal:
160+
principal: User:my-test-service
134161
operation: READ
135162
permission: ALLOW
136163
read-all-service:
137164
name: service.
138165
type: TOPIC
139166
pattern: PREFIXED
140167
host: "*"
141-
principal:
168+
principal: User:my-test-service
169+
operation: READ
170+
permission: ALLOW
171+
```
172+
173+
## Custom User ACLs
174+
175+
**Synopsis**: Define custom ACLs for a specific user.
176+
177+
For example, if a specific user needs to produce to all topics prefixed with `kafka.` and `service.`, you may not want to define them all in your desired state file.
178+
179+
If you have a user `my-test-user` defined, you can define custom ACLs as so:
180+
181+
```yaml
182+
customUserAcls:
183+
my-test-user:
184+
read-all-kafka:
185+
name: kafka.
186+
type: TOPIC
187+
pattern: PREFIXED
188+
host: "*"
142189
operation: READ
143190
permission: ALLOW
144191
```
192+
193+
?> **NOTE**: The `principal` field can be left out here and it will be inherited from the user definition.
194+

src/main/java/com/devshawn/kafka/gitops/StateManager.java

+38-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
2828
import org.slf4j.LoggerFactory;
2929

30-
import java.util.*;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.NoSuchElementException;
34+
import java.util.Optional;
3135
import java.util.concurrent.atomic.AtomicInteger;
3236
import java.util.concurrent.atomic.AtomicReference;
3337

@@ -131,6 +135,7 @@ private DesiredState getDesiredState() {
131135
generateConfluentCloudUserAcls(desiredState, desiredStateFile);
132136
} else {
133137
generateServiceAcls(desiredState, desiredStateFile);
138+
generateUserAcls(desiredState, desiredStateFile);
134139
}
135140

136141
return desiredState.build();
@@ -173,6 +178,15 @@ private void generateConfluentCloudUserAcls(DesiredState.Builder desiredState, D
173178
List<AclDetails.Builder> acls = roleService.getAcls(role, String.format("User:%s", serviceAccountId));
174179
acls.forEach(acl -> desiredState.putAcls(String.format("%s-%s", name, index.getAndSet(index.get() + 1)), acl.build()));
175180
});
181+
182+
if (desiredStateFile.getCustomUserAcls().containsKey(name)) {
183+
Map<String, CustomAclDetails> customAcls = desiredStateFile.getCustomUserAcls().get(name);
184+
customAcls.forEach((aclName, customAcl) -> {
185+
AclDetails.Builder aclDetails = AclDetails.fromCustomAclDetails(customAcl);
186+
aclDetails.setPrincipal(String.format("User:%s", serviceAccountId));
187+
desiredState.putAcls(String.format("%s-%s", name, index.getAndSet(index.get() + 1)), aclDetails.build());
188+
});
189+
}
176190
});
177191
}
178192

@@ -188,7 +202,29 @@ private void generateServiceAcls(DesiredState.Builder desiredState, DesiredState
188202
customAcls.forEach((aclName, customAcl) -> {
189203
AclDetails.Builder aclDetails = AclDetails.fromCustomAclDetails(customAcl);
190204
aclDetails.setPrincipal(customAcl.getPrincipal().orElseThrow(() ->
191-
new MissingConfigurationException(String.format("Missing principal for custom ACL %s", aclName))));
205+
new MissingConfigurationException(String.format("Missing principal for custom service ACL %s", aclName))));
206+
desiredState.putAcls(String.format("%s-%s", name, index.getAndSet(index.get() + 1)), aclDetails.build());
207+
});
208+
}
209+
});
210+
}
211+
212+
private void generateUserAcls(DesiredState.Builder desiredState, DesiredStateFile desiredStateFile) {
213+
desiredStateFile.getUsers().forEach((name, user) -> {
214+
AtomicReference<Integer> index = new AtomicReference<>(0);
215+
String userPrincipal = user.getPrincipal()
216+
.orElseThrow(() -> new MissingConfigurationException(String.format("Missing principal for user %s", name)));
217+
218+
user.getRoles().forEach(role -> {
219+
List<AclDetails.Builder> acls = roleService.getAcls(role, userPrincipal);
220+
acls.forEach(acl -> desiredState.putAcls(String.format("%s-%s", name, index.getAndSet(index.get() + 1)), acl.build()));
221+
});
222+
223+
if (desiredStateFile.getCustomUserAcls().containsKey(name)) {
224+
Map<String, CustomAclDetails> customAcls = desiredStateFile.getCustomUserAcls().get(name);
225+
customAcls.forEach((aclName, customAcl) -> {
226+
AclDetails.Builder aclDetails = AclDetails.fromCustomAclDetails(customAcl);
227+
aclDetails.setPrincipal(customAcl.getPrincipal().orElse(userPrincipal));
192228
desiredState.putAcls(String.format("%s-%s", name, index.getAndSet(index.get() + 1)), aclDetails.build());
193229
});
194230
}

src/main/java/com/devshawn/kafka/gitops/domain/state/DesiredStateFile.java

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public interface DesiredStateFile {
2222

2323
Map<String, Map<String, CustomAclDetails>> getCustomServiceAcls();
2424

25+
Map<String, Map<String, CustomAclDetails>> getCustomUserAcls();
26+
2527
class Builder extends DesiredStateFile_Builder {
2628
}
2729
}

src/main/java/com/devshawn/kafka/gitops/domain/state/UserDetails.java

+3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import org.inferred.freebuilder.FreeBuilder;
55

66
import java.util.List;
7+
import java.util.Optional;
78

89
@FreeBuilder
910
@JsonDeserialize(builder = UserDetails.Builder.class)
1011
public interface UserDetails {
1112

13+
Optional<String> getPrincipal();
14+
1215
List<String> getRoles();
1316

1417
class Builder extends UserDetails_Builder {

src/test/groovy/com/devshawn/kafka/gitops/ApplyCommandIntegrationSpec.groovy

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ class ApplyCommandIntegrationSpec extends Specification {
4848
planFile << [
4949
"simple",
5050
"application-service",
51-
"multi-file"
51+
"multi-file",
52+
"simple-users",
53+
"custom-service-acls",
54+
"custom-user-acls"
5255
]
5356
}
5457

src/test/groovy/com/devshawn/kafka/gitops/PlanCommandIntegrationSpec.groovy

+6-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ class PlanCommandIntegrationSpec extends Specification {
5555
"kafka-connect-service",
5656
"kafka-streams-service",
5757
"topics-and-services",
58-
"multi-file"
58+
"multi-file",
59+
"simple-users",
60+
"custom-service-acls",
61+
"custom-user-acls"
5962
]
6063
}
6164

@@ -123,7 +126,8 @@ class PlanCommandIntegrationSpec extends Specification {
123126
"invalid-missing-principal",
124127
"invalid-topic",
125128
"unrecognized-property",
126-
"invalid-format"
129+
"invalid-format",
130+
"invalid-missing-user-principal"
127131
]
128132
}
129133

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Executing apply...
2+
3+
Applying: [CREATE]
4+
5+
+ [ACL] test-service-0
6+
+ resource_name: kafka.
7+
+ resource_type: TOPIC
8+
+ resource_pattern: PREFIXED
9+
+ resource_principal: User:test
10+
+ host: *
11+
+ operation: READ
12+
+ permission: ALLOW
13+
14+
15+
Successfully applied.
16+
17+
[SUCCESS] Apply complete! Resources: 1 created, 0 updated, 0 deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"topicPlans": [],
3+
"aclPlans": [
4+
{
5+
"name": "test-service-0",
6+
"aclDetails": {
7+
"name": "kafka.",
8+
"type": "TOPIC",
9+
"pattern": "PREFIXED",
10+
"principal": "User:test",
11+
"host": "*",
12+
"operation": "READ",
13+
"permission": "ALLOW"
14+
},
15+
"action": "ADD"
16+
}
17+
]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
services:
2+
test-service:
3+
type: application
4+
principal: User:test
5+
6+
customServiceAcls:
7+
test-service:
8+
read-all-kafka:
9+
name: kafka.
10+
type: TOPIC
11+
pattern: PREFIXED
12+
host: "*"
13+
principal: User:test
14+
operation: READ
15+
permission: ALLOW
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
Executing apply...
2+
3+
Applying: [CREATE]
4+
5+
+ [ACL] test-user-0
6+
+ resource_name: kafka.
7+
+ resource_type: TOPIC
8+
+ resource_pattern: PREFIXED
9+
+ resource_principal: User:test
10+
+ host: *
11+
+ operation: READ
12+
+ permission: ALLOW
13+
14+
15+
Successfully applied.
16+
17+
[SUCCESS] Apply complete! Resources: 1 created, 0 updated, 0 deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"topicPlans": [],
3+
"aclPlans": [
4+
{
5+
"name": "test-user-0",
6+
"aclDetails": {
7+
"name": "kafka.",
8+
"type": "TOPIC",
9+
"pattern": "PREFIXED",
10+
"principal": "User:test",
11+
"host": "*",
12+
"operation": "READ",
13+
"permission": "ALLOW"
14+
},
15+
"action": "ADD"
16+
}
17+
]
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
users:
2+
test-user:
3+
principal: User:test
4+
5+
customUserAcls:
6+
test-user:
7+
read-all-kafka:
8+
name: kafka.
9+
type: TOPIC
10+
pattern: PREFIXED
11+
host: "*"
12+
operation: READ
13+
permission: ALLOW
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Generating execution plan...
2+
3+
[ERROR] Missing required configuration: Missing principal for user test-user
4+
5+
[ERROR] An error has occurred during the planning process. No plan was created.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
users:
2+
test-user:
3+
roles:
4+
- reader

0 commit comments

Comments
 (0)