Skip to content

Commit f3c6ade

Browse files
committed
commit what has been developed today
1 parent 6104019 commit f3c6ade

File tree

3 files changed

+248
-7
lines changed

3 files changed

+248
-7
lines changed

operator/src/main/java/it/aboutbits/postgresql/core/PostgreSQLContextFactory.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,19 @@ public class PostgreSQLContextFactory {
2020
private final KubernetesService kubernetesService;
2121
private final KubernetesClient kubernetesClient;
2222

23+
/// Create a DSLContext with a JDBC connection to the PostgreSQL maintenance database.
2324
public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) {
25+
return getDSLContext(
26+
clusterConnection,
27+
clusterConnection.getSpec().getMaintenanceDatabase()
28+
);
29+
}
30+
31+
/// Create a DSLContext with a JDBC connection to the specified database.
32+
public CloseableDSLContext getDSLContext(
33+
ClusterConnection clusterConnection,
34+
String database
35+
) {
2436
var credentials = kubernetesService.getSecretRefCredentials(
2537
kubernetesClient,
2638
clusterConnection
@@ -31,7 +43,7 @@ public CloseableDSLContext getDSLContext(ClusterConnection clusterConnection) {
3143
var jdbcUrl = "jdbc:postgresql://%s:%d/%s".formatted(
3244
spec.getHost(),
3345
spec.getPort(),
34-
spec.getMaintenanceDatabase()
46+
database
3547
);
3648

3749
var properties = new Properties(2 + spec.getParameters().size());

operator/src/main/java/it/aboutbits/postgresql/crd/grant/GrantObjectType.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import com.google.errorprone.annotations.Immutable;
44
import it.aboutbits.postgresql.core.infrastructure.persistence.Routines;
55
import lombok.Getter;
6-
import lombok.RequiredArgsConstructor;
76
import lombok.experimental.Accessors;
87
import org.jooq.Field;
98
import org.jspecify.annotations.NullMarked;
109

1110
import java.util.List;
11+
import java.util.Set;
1212

1313
import static it.aboutbits.postgresql.crd.grant.GrantPrivilege.ALTER_SYSTEM;
1414
import static it.aboutbits.postgresql.crd.grant.GrantPrivilege.CONNECT;
@@ -34,7 +34,6 @@
3434
@NullMarked
3535
@Getter
3636
@Accessors(fluent = true)
37-
@RequiredArgsConstructor
3837
public enum GrantObjectType {
3938
DATABASE(
4039
Routines::hasDatabasePrivilege1,
@@ -122,11 +121,23 @@ public enum GrantObjectType {
122121
)
123122
);
124123

124+
/// [Access Privilege Inquiry Functions](https://www.postgresql.org/docs/current/functions-info.html#FUNCTIONS-INFO-ACCESS)
125125
private final PrivilegeFunction checkPrivilegeFunction;
126126

127127
@SuppressWarnings("ImmutableEnumChecker")
128128
private final List<GrantPrivilege> privileges;
129129

130+
private final Set<GrantPrivilege> privilegesSet;
131+
132+
GrantObjectType(
133+
PrivilegeFunction checkPrivilegeFunction,
134+
List<GrantPrivilege> privileges
135+
) {
136+
this.checkPrivilegeFunction = checkPrivilegeFunction;
137+
this.privileges = privileges;
138+
this.privilegesSet = Set.copyOf(privileges);
139+
}
140+
130141
@Immutable
131142
@FunctionalInterface
132143
public interface PrivilegeFunction {
Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,242 @@
11
package it.aboutbits.postgresql.crd.grant;
22

3+
import io.fabric8.kubernetes.client.KubernetesClient;
34
import io.javaoperatorsdk.operator.api.reconciler.Context;
45
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
56
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
67
import it.aboutbits.postgresql.core.BaseReconciler;
8+
import it.aboutbits.postgresql.core.CRPhase;
79
import it.aboutbits.postgresql.core.CRStatus;
8-
import it.aboutbits.postgresql.crd.role.Role;
10+
import it.aboutbits.postgresql.core.PostgreSQLContextFactory;
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import org.jooq.DSLContext;
14+
import org.jooq.Name;
15+
import org.jooq.Privilege;
16+
import org.jooq.Record3;
17+
import org.jooq.Select;
918
import org.jspecify.annotations.NullMarked;
1019

20+
import java.util.ArrayList;
21+
import java.util.HashSet;
22+
import java.util.Locale;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.stream.Collectors;
25+
26+
import static org.jooq.impl.DSL.grant;
27+
import static org.jooq.impl.DSL.privilege;
28+
import static org.jooq.impl.DSL.quotedName;
29+
import static org.jooq.impl.DSL.role;
30+
import static org.jooq.impl.DSL.select;
31+
import static org.jooq.impl.DSL.val;
32+
1133
@NullMarked
34+
@Slf4j
35+
@RequiredArgsConstructor
1236
public class GrantReconciler
13-
extends BaseReconciler<Role, CRStatus>
37+
extends BaseReconciler<Grant, CRStatus>
1438
implements Reconciler<Grant> {
39+
private static final String OBJECT_FIELD_NAME = "object";
40+
private static final String PRIVILEGE_FIELD_NAME = "privilege";
41+
private static final String GRANTED_FIELD_NAME = "granted";
42+
43+
private final KubernetesClient kubernetesClient;
44+
private final PostgreSQLContextFactory contextFactory;
45+
1546
@Override
16-
public UpdateControl<Grant> reconcile(Grant resource, Context<Grant> context) {
17-
return UpdateControl.noUpdate();
47+
public UpdateControl<Grant> reconcile(
48+
Grant resource,
49+
Context<Grant> context
50+
) {
51+
var spec = resource.getSpec();
52+
var status = initializeStatus(resource);
53+
54+
var name = resource.getMetadata().getName();
55+
var namespace = resource.getMetadata().getNamespace();
56+
57+
log.info(
58+
"Reconciling Grant [resource={}/{}, status.phase={}]",
59+
namespace,
60+
name,
61+
status.getPhase()
62+
);
63+
64+
var clusterRef = spec.getClusterRef();
65+
66+
var clusterConnectionOptional = getReferencedClusterConnection(
67+
kubernetesClient,
68+
resource,
69+
clusterRef
70+
);
71+
72+
var objectType = spec.getObjectType();
73+
var grantPrivileges = new HashSet<>(spec.getPrivileges());
74+
var allowedPrivilegesForObjectType = objectType.privilegesSet();
75+
76+
if (!allowedPrivilegesForObjectType.containsAll(grantPrivileges)) {
77+
var invalid = new HashSet<>(grantPrivileges);
78+
79+
invalid.removeAll(allowedPrivilegesForObjectType);
80+
81+
status.setPhase(CRPhase.ERROR)
82+
.setMessage("Grant contains invalid privileges for the specified object type [resource=%s/%s, objectType=%s, invalidPrivileges=%s, allowedPrivilegesForObjectType=%s]".formatted(
83+
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
84+
clusterRef.getName(),
85+
objectType,
86+
invalid,
87+
objectType.privileges()
88+
));
89+
90+
return UpdateControl.patchStatus(resource);
91+
}
92+
93+
if (clusterConnectionOptional.isEmpty()) {
94+
status.setPhase(CRPhase.PENDING)
95+
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
96+
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
97+
clusterRef.getName()
98+
));
99+
100+
return UpdateControl.patchStatus(resource)
101+
.rescheduleAfter(60, TimeUnit.SECONDS);
102+
}
103+
104+
var database = spec.getDatabase();
105+
var clusterConnection = clusterConnectionOptional.get();
106+
107+
UpdateControl<Grant> updateControl;
108+
109+
try (var dsl = contextFactory.getDSLContext(clusterConnection, database)) {
110+
// Run everything in a single transaction
111+
updateControl = dsl.transactionResult(
112+
cfg -> reconcileInTransaction(
113+
cfg.dsl(),
114+
resource,
115+
status
116+
)
117+
);
118+
} catch (Exception e) {
119+
return handleError(
120+
resource,
121+
status,
122+
e
123+
);
124+
}
125+
126+
return updateControl;
127+
}
128+
129+
private UpdateControl<Grant> reconcileInTransaction(
130+
DSLContext tx,
131+
Grant resource,
132+
CRStatus status
133+
) {
134+
var name = resource.getMetadata().getName();
135+
var namespace = resource.getMetadata().getNamespace();
136+
137+
var spec = resource.getSpec();
138+
139+
var role = spec.getRole();
140+
var schema = spec.getSchema();
141+
var objectType = spec.getObjectType();
142+
var objects = spec.getObjects();
143+
var grantPrivileges = spec.getPrivileges();
144+
145+
var checkPrivilegeFunction = objectType.checkPrivilegeFunction();
146+
147+
var checks = new ArrayList<Select<Record3<String, String, Boolean>>>(
148+
objects.size() * grantPrivileges.size()
149+
);
150+
151+
for (var object : objects) {
152+
var qualifiedObject = quotedName(schema, object);
153+
var renderedObject = tx.render(qualifiedObject);
154+
155+
for (var grantPrivilege : grantPrivileges) {
156+
var privilege = grantPrivilege.name().toLowerCase(Locale.ROOT);
157+
158+
var hasPrivilegeFunctionCall = checkPrivilegeFunction.apply(
159+
role,
160+
renderedObject,
161+
privilege
162+
);
163+
164+
// Select the object name and privilege string alongside the result
165+
// so we can map the answer back to the correct key.
166+
checks.add(
167+
select(
168+
val(object).as(OBJECT_FIELD_NAME),
169+
val(privilege).as(PRIVILEGE_FIELD_NAME),
170+
hasPrivilegeFunctionCall.as(GRANTED_FIELD_NAME)
171+
)
172+
);
173+
}
174+
}
175+
176+
// 2. Execute all checks in a single round-trip using UNION ALL
177+
if (checks.isEmpty()) {
178+
// TODO nothing to do
179+
}
180+
181+
var batchQuery = checks.stream()
182+
.reduce(Select::unionAll)
183+
.orElseThrow();
184+
185+
var objectPrivilegeIsGrantedMap = tx.fetch(batchQuery)
186+
.stream()
187+
.collect(Collectors.toUnmodifiableMap(
188+
result -> {
189+
var objectName = result.get(OBJECT_FIELD_NAME, String.class);
190+
var privilegeName = result.get(PRIVILEGE_FIELD_NAME, String.class);
191+
192+
return new ObjectPrivilege(
193+
quotedName(schema, objectName),
194+
privilege(privilegeName)
195+
);
196+
},
197+
result -> result.get(GRANTED_FIELD_NAME, Boolean.class)
198+
));
199+
200+
var grants = objectPrivilegeIsGrantedMap.entrySet()
201+
.stream()
202+
.map(entry -> {
203+
var objectPrivilege = entry.getKey();
204+
//var isGranted = entry.getValue();
205+
206+
log.info(
207+
"Granting privilege [resource={}/{}, role={}, object={}, privilege={}]",
208+
namespace,
209+
name,
210+
role,
211+
objectPrivilege.qualifiedObject(),
212+
objectPrivilege.privilege()
213+
);
214+
215+
var privilege = objectPrivilege.privilege();
216+
var qualifiedObject = objectPrivilege.qualifiedObject();
217+
218+
return grant(privilege)
219+
.on(qualifiedObject)
220+
.to(role(role));
221+
})
222+
.toList();
223+
224+
tx.batch(grants).execute();
225+
226+
status.setPhase(CRPhase.READY)
227+
.setMessage(null);
228+
229+
return UpdateControl.patchStatus(resource);
18230
}
19231

20232
@Override
21233
protected CRStatus newStatus() {
22234
return new CRStatus();
23235
}
236+
237+
private record ObjectPrivilege(
238+
Name qualifiedObject,
239+
Privilege privilege
240+
) {
241+
}
24242
}

0 commit comments

Comments
 (0)