Skip to content

Commit 89b4957

Browse files
author
Gerald Unterrainer
committed
fix up and update README.md
1 parent 08832c0 commit 89b4957

21 files changed

+471
-140
lines changed

Diff for: README.md

+84
Original file line numberDiff line numberDiff line change
@@ -421,3 +421,87 @@ List<NexusUserJpa> g = query.getN(22);
421421
NexusUserJpa h = query.getSingle();
422422
```
423423
424+
#### Tenant Capability
425+
426+
The system allows to save different tennants within the same tables, in order to ease development.
427+
428+
This is done on a per-DAO basis (per -table so to say), as there has to be a separate table holding permission-data regarding the tenant-IDs and corresponding reference-IDs.
429+
430+
```bash
431+
--- Person
432+
id (Long)
433+
name (String)
434+
435+
--- Person_Permission
436+
id (Long)
437+
referenceId (Long)
438+
tenantId (Long)
439+
440+
----------------------------------------
441+
--- Person
442+
1 Peter
443+
2 Paul
444+
3 Mary
445+
446+
--- Person_Permission
447+
1 1 1
448+
2 2 1
449+
2 3 2
450+
```
451+
452+
When a user having tenantId=1 associated will query the full list of Persons, he will inevitably receive 'Peter and Paul', whereas another user associated with tennantId=2 would receive 'Mary'.
453+
454+
##### Data-Side
455+
456+
To enable this feature, you have to generate a special DAO that is linked to the corresponding permissions-DAO by specifying the JPA-type of the permission-DAO and both the name of the reference-ID and tenant-ID field (all that is explained in the constructor of the DAO).
457+
458+
So you have to create the appropriate permission-table, a permission-JPA for the table.
459+
460+
##### User-Side
461+
462+
In order to query those tables accordingly, your querying user has to be associated with a tenant-ID.
463+
464+
In fact there are TWO associations with multiple tenant-IDs there.
465+
The `tenant_read` set, used to determine if a user can see (and therefore modify or delete) a row, and the `tenant_write` set, used to determine how many and which permission-rows there are to write when creating a new row in the main-table.
466+
467+
###### Setting permissions in the DAO
468+
469+
On the DAO-level (if you do DB stuff on the server) you may specify those freely using the according setters in the query-builders of the DAO. When using the DAO you will only have to specify a single set of tenant-IDs since you know how you're planning on using those yourself (if you will create a row, then the tenant-ID set is equivalent to the `tenant_write` set; if you will only query, then specify a tenant-ID set equivalent to the `tenant_read` set).
470+
471+
###### Setting permissions via KeyCloak
472+
473+
In KeyCloak you have to specify both sets per user and the system will pick the appropriate set when manipulating or querying the database.
474+
475+
This is done by settings User-Attributes.
476+
477+
```bash
478+
User: Psilo / Attributes
479+
-- tenant_read: 1,3
480+
-- tenant_write: 1
481+
```
482+
483+
On KeyCloak-Setup, be advised that you have to specify an Attribute-Mapper for both attributes (`Clients-><ClientName>->Mappers, create with name=tenants_read/tenants_write, User Attribute=<name>, Token Claim Name=<name>, Claim JSON Type=String`).
484+
485+
That set up, the Attribute values will be passed on into the JWT token and parsed by Http-Server (the data will be copied to the Context.Attribute Object from where you may retrieve them at any time during a request).
486+
The system will decide automatically which set to use, so that in this example the user `Psilo` will be able to see rows that have the permission for tenant-ID 1 or 3, but when creating a row, it will only write a permission for tenant-ID 1.
487+
488+
##### Example
489+
490+
```java
491+
// Passing the TestPermissionJpa class enables the tenant-capability.
492+
// The TestPermissionJpa has a getReferenceId() and getTenantId() method
493+
// as required by the default setting.
494+
server.handlerGroupFor(TestJpa.class, TestJson.class,
495+
new JpqlDao<>(emf, TestJpa.class, TestPermissionJpa.class))
496+
.path("/tenanttests")
497+
.endpoints(Endpoint.ALL)
498+
.jsonMapper(mapper)
499+
.addRoleFor(Endpoint.ALL, RoleBuilder.authenticated())...
500+
501+
// The next example sets up tenant-capability using a JPA that has a
502+
// getRefId() and a getTId() method (not default).
503+
server.handlerGroupFor(TestJpa.class, TestJson.class,
504+
new JpqlDao<>(emf, TestJpa.class, TestPermissionJpa.class, "refId", "tId"))
505+
.path("/tenanttests")
506+
```
507+

Diff for: src/main/java/info/unterrainer/commons/httpserver/GenericHandlerGroup.java

+4-9
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ public class GenericHandlerGroup<P extends BasicJpa, J extends BasicJson, E> imp
4444
private final LinkedHashMap<Endpoint, Role[]> accessRoles;
4545
private final ExecutorService executorService;
4646
private final HandlerUtils hu = new HandlerUtils();
47-
private final String tenantIdRowName;
48-
private final CoreDao<? extends BasicJpa, E> tenantDao;
49-
private final Class<? extends BasicJpa> tenantJpaType;
50-
private final String fieldRowName;
51-
private final String tenantRowName;
5247

5348
@Override
5449
public void addHandlers(final HttpServer server) {
@@ -132,7 +127,7 @@ private void getList(final Context ctx) {
132127

133128
ListJson<P> bList = dao.getList(transaction.getManager(), offset, size, interceptorResult.getSelectClause(),
134129
interceptorResult.getJoinClause(), interceptorResult.getWhereClause(), interceptorResult.getParams(),
135-
interceptorResult.getOrderByClause());
130+
interceptorResult.getOrderByClause(), hu.getReadTenantIdsFrom(ctx));
136131
ListJson<J> jList = new ListJson<>();
137132
for (P entry : bList.getEntries())
138133
jList.getEntries().add(orikaMapper.map(entry, jsonType));
@@ -153,7 +148,7 @@ private void create(final Context ctx) throws IOException {
153148
DaoTransaction<E> transaction = dao.getTransactionManager().beginTransaction();
154149

155150
mappedJpa = extensions.runPreInsert(ctx, transaction.getManager(), json, mappedJpa, executorService);
156-
P createdJpa = dao.create(transaction.getManager(), mappedJpa);
151+
P createdJpa = dao.create(transaction.getManager(), mappedJpa, hu.getWriteTenantIdsFrom(ctx));
157152
J r = orikaMapper.map(createdJpa, jsonType);
158153

159154
r = extensions.runPostInsert(ctx, transaction.getManager(), json, mappedJpa, createdJpa, r,
@@ -179,7 +174,7 @@ private void fullUpdate(final Context ctx) throws IOException {
179174

180175
mappedJpa = extensions.runPreModify(ctx, transaction.getManager(), jpa.getId(), json, jpa, mappedJpa,
181176
executorService);
182-
P persistedJpa = dao.update(transaction.getManager(), mappedJpa);
177+
P persistedJpa = dao.update(transaction.getManager(), mappedJpa, hu.getReadTenantIdsFrom(ctx));
183178

184179
J r = orikaMapper.map(persistedJpa, jsonType);
185180
r = extensions.runPostModify(ctx, transaction.getManager(), jpa.getId(), json, jpa, mappedJpa, persistedJpa,
@@ -200,7 +195,7 @@ private void delete(final Context ctx) {
200195
DaoTransaction<E> transaction = dao.getTransactionManager().beginTransaction();
201196

202197
id = extensions.runPreDelete(ctx, transaction.getManager(), id, executorService);
203-
dao.delete(transaction.getManager(), id);
198+
dao.delete(transaction.getManager(), id, hu.getReadTenantIdsFrom(ctx));
204199
ctx.status(204);
205200
extensions.runPostDelete(ctx, transaction.getManager(), id, executorService);
206201

Diff for: src/main/java/info/unterrainer/commons/httpserver/GenericHandlerGroupBuilder.java

+1-23
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,6 @@ public class GenericHandlerGroupBuilder<P extends BasicJpa, J extends BasicJson,
3737
private List<GetListInterceptor> getListInterceptors = new ArrayList<>();
3838
private ExecutorService executorService;
3939

40-
private String tenantIdRowName;
41-
private String tenantRowName;
42-
private String tenantFieldRowName;
43-
private CoreDao<? extends BasicJpa, E> tenantDao;
44-
private Class<? extends BasicJpa> tenantJpaType;
45-
4640
HandlerExtensions<P, J, E> extensions = new HandlerExtensions<>();
4741
private LinkedHashMap<Endpoint, Role[]> accessRoles = new LinkedHashMap<>();
4842

@@ -55,8 +49,7 @@ public HttpServer add() {
5549
executorService = server.executorService;
5650
GenericHandlerGroup<P, J, E> handlerGroupInstance = new GenericHandlerGroup<>(dao, jpaType, jsonType,
5751
jsonMapper, orikaFactory.getMapperFacade(), path, endpoints, getListInterceptors, extensions,
58-
accessRoles, executorService, tenantIdRowName, tenantDao, tenantJpaType, tenantFieldRowName,
59-
tenantRowName);
52+
accessRoles, executorService);
6053
server.addHandlerGroup(handlerGroupInstance);
6154
return server;
6255
}
@@ -70,21 +63,6 @@ public GenericHandlerGroupBuilder<P, J, E> addRoleFor(final Endpoint endpoint, f
7063
return this;
7164
}
7265

73-
public GenericHandlerGroupBuilder<P, J, E> isMultiTenantEnabledByIdRow(final String tenantIdRowName) {
74-
this.tenantIdRowName = tenantIdRowName;
75-
return this;
76-
}
77-
78-
public <TP extends BasicJpa> GenericHandlerGroupBuilder<P, J, E> isMultiTenantEnabledByTable(
79-
final CoreDao<TP, E> tenantDao, final Class<TP> tenantJpaType, final String fieldRowName,
80-
final String tenantRowName) {
81-
this.tenantDao = tenantDao;
82-
this.tenantJpaType = tenantJpaType;
83-
this.tenantFieldRowName = fieldRowName;
84-
this.tenantRowName = tenantRowName;
85-
return this;
86-
}
87-
8866
public GenericHandlerGroupBuilder<P, J, E> jsonMapper(final JsonMapper jsonMapper) {
8967
this.jsonMapper = jsonMapper;
9068
return this;

Diff for: src/main/java/info/unterrainer/commons/httpserver/HandlerUtils.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import java.time.LocalDateTime;
44
import java.time.format.DateTimeFormatter;
55
import java.time.format.DateTimeParseException;
6+
import java.util.Set;
67

78
import info.unterrainer.commons.httpserver.daos.CoreDao;
89
import info.unterrainer.commons.httpserver.daos.DaoTransaction;
10+
import info.unterrainer.commons.httpserver.enums.Attribute;
911
import info.unterrainer.commons.httpserver.enums.QueryField;
1012
import info.unterrainer.commons.httpserver.exceptions.BadRequestException;
1113
import info.unterrainer.commons.httpserver.exceptions.NotFoundException;
@@ -22,17 +24,25 @@ public Long checkAndGetId(final Context ctx) {
2224

2325
public <P extends BasicJpa, E> P getJpaById(final Context ctx, final E manager, final CoreDao<P, E> dao) {
2426
Long id = checkAndGetId(ctx);
25-
P jpa = dao.getById(manager, id);
27+
P jpa = dao.getById(manager, id, getReadTenantIdsFrom(ctx));
2628
if (jpa == null)
2729
throw new NotFoundException();
2830
return jpa;
2931
}
3032

33+
public Set<Long> getReadTenantIdsFrom(final Context ctx) {
34+
return ctx.attribute(Attribute.USER_TENANTS_READ_SET);
35+
}
36+
37+
public Set<Long> getWriteTenantIdsFrom(final Context ctx) {
38+
return ctx.attribute(Attribute.USER_TENANTS_WRITE_SET);
39+
}
40+
3141
public <P extends BasicJpa, E> P getJpaById(final Context ctx, final CoreDao<P, E> dao) {
3242
Long id = checkAndGetId(ctx);
3343
DaoTransaction<E> transaction = dao.getTransactionManager().beginTransaction();
3444
try {
35-
P jpa = dao.getById(transaction.getManager(), id);
45+
P jpa = dao.getById(transaction.getManager(), id, getReadTenantIdsFrom(ctx));
3646
if (jpa == null)
3747
throw new NotFoundException();
3848
return jpa;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package info.unterrainer.commons.httpserver.daos;
2+
3+
import java.util.Arrays;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
7+
import javax.persistence.EntityManagerFactory;
8+
9+
import info.unterrainer.commons.rdbutils.entities.BasicAsyncJpa;
10+
import info.unterrainer.commons.rdbutils.enums.AsyncState;
11+
12+
public class AsyncJpaListQueryBuilder<P extends BasicAsyncJpa>
13+
extends BasicListQueryBuilder<P, P, AsyncJpaListQueryBuilder<P>> implements QueryInterface<P, P> {
14+
15+
private Set<AsyncState> asyncStates = new HashSet<>();
16+
17+
AsyncJpaListQueryBuilder(final EntityManagerFactory emf, final AsyncJpqlDao<P> dao, final Class<P> resultType) {
18+
super(emf, dao, resultType);
19+
}
20+
21+
public JpaListQuery<P> build() {
22+
return new JpaListQuery<>(emf, this);
23+
}
24+
25+
/**
26+
* Adds a single or multiple OR-clauses containing the specified
27+
* {@link AsyncState}s.
28+
* <p>
29+
* For example calling
30+
* {@code .whereStateOf(AsyncState.NEW, AsyncState.PROCESSING)} will result in
31+
* the where-clause
32+
* {@code (..rest of where clause..) AND (state = 'NEW' OR state = 'PROCESSING').}
33+
* <p>
34+
* Repetitive calling of this method just adds all the AsyncState values
35+
* (doesn't clear the collection).
36+
*
37+
* @param asyncStates a single or number of AsyncState values
38+
* @return an instance of this builder to provide a fluent interface
39+
*/
40+
public AsyncJpaListQueryBuilder<P> asyncStateOf(final AsyncState... asyncStates) {
41+
this.asyncStates.addAll(Arrays.asList(asyncStates));
42+
return this;
43+
}
44+
}

Diff for: src/main/java/info/unterrainer/commons/httpserver/daos/AsyncJpqlDao.java

+46-4
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,51 @@
33
import javax.persistence.EntityManagerFactory;
44

55
import info.unterrainer.commons.rdbutils.entities.BasicAsyncJpa;
6+
import info.unterrainer.commons.rdbutils.entities.BasicJpa;
67

78
public class AsyncJpqlDao<P extends BasicAsyncJpa> extends BasicJpqlDao<P> {
89

10+
/**
11+
* Generates a DAO that lets you build and execute queries.
12+
* <p>
13+
* This dao has a tenant-permission table attached and may retrieve and write
14+
* data per tenant.
15+
* <p>
16+
* {@code tenantReferenceFieldName} defaults to {@code referenceId}<br>
17+
* {@code tenantReferenceFieldName} defaults to {@code tenantId}
18+
*
19+
* @param emf the {@link EntityManagerFactory} to use
20+
* @param type the return-type of the query (the underlying JPA)
21+
* @param tenantJpaType the JPA of the tenant-permission table associated
22+
*/
23+
public AsyncJpqlDao(final EntityManagerFactory emf, final Class<P> type,
24+
final Class<? extends BasicJpa> tenantJpaType) {
25+
super(emf, type);
26+
this.coreDao.tenantData = new TenantData(tenantJpaType);
27+
}
28+
29+
/**
30+
* Generates a DAO that lets you build and execute queries.
31+
* <p>
32+
* This dao has a tenant-permission table attached and may retrieve and write
33+
* data per tenant.
34+
*
35+
* @param emf the {@link EntityManagerFactory} to use
36+
* @param type the return-type of the query (the underlying
37+
* JPA)
38+
* @param tenantJpaType the JPA of the tenant-permission table
39+
* associated
40+
* @param tenantReferenceFieldName the name of the field holding the reference
41+
* to the main-table-id
42+
* @param tenantIdFieldName the name of the field holding the tenant-ID
43+
*/
44+
public AsyncJpqlDao(final EntityManagerFactory emf, final Class<P> type,
45+
final Class<? extends BasicJpa> tenantJpaType, final String tenantReferenceFieldName,
46+
final String tenantIdFieldName) {
47+
super(emf, type);
48+
this.coreDao.tenantData = new TenantData(tenantJpaType, tenantReferenceFieldName, tenantIdFieldName);
49+
}
50+
951
/**
1052
* Generates a DAO that lets you build and execute queries.
1153
*
@@ -22,8 +64,8 @@ public AsyncJpqlDao(final EntityManagerFactory emf, final Class<P> type) {
2264
*
2365
* @return a query-builder
2466
*/
25-
public AsyncListQueryBuilder<P, P> select() {
26-
return new AsyncListQueryBuilder<>(emf, this, type);
67+
public AsyncJpaListQueryBuilder<P> select() {
68+
return new AsyncJpaListQueryBuilder<>(emf, this, type);
2769
}
2870

2971
/**
@@ -64,8 +106,8 @@ public <T> AsyncListQueryBuilder<P, T> select(final String selectClause, final C
64106
* a "SELECT o")
65107
* @return a query-builder
66108
*/
67-
public AsyncListQueryBuilder<P, P> select(final String selectClause) {
68-
AsyncListQueryBuilder<P, P> b = new AsyncListQueryBuilder<>(emf, this, type);
109+
public AsyncJpaListQueryBuilder<P> select(final String selectClause) {
110+
AsyncJpaListQueryBuilder<P> b = new AsyncJpaListQueryBuilder<>(emf, this, type);
69111
b.setSelect(selectClause);
70112
return b;
71113
}

Diff for: src/main/java/info/unterrainer/commons/httpserver/daos/BasicJpqlDao.java

+5-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package info.unterrainer.commons.httpserver.daos;
22

3-
import java.time.LocalDateTime;
43
import java.util.List;
4+
import java.util.Set;
55

66
import javax.persistence.EntityManager;
77
import javax.persistence.EntityManagerFactory;
88
import javax.persistence.TypedQuery;
99

10-
import info.unterrainer.commons.jreutils.DateUtils;
1110
import info.unterrainer.commons.rdbutils.entities.BasicJpa;
1211
import lombok.Getter;
1312

@@ -25,12 +24,6 @@ public BasicJpqlDao(final EntityManagerFactory emf, final Class<P> type) {
2524
coreDao = new JpqlCoreDao<>(emf, type);
2625
}
2726

28-
P update(final EntityManager em, final P entity) {
29-
LocalDateTime time = DateUtils.nowUtc();
30-
entity.setEditedOn(time);
31-
return em.merge(entity);
32-
}
33-
3427
<T> List<T> getList(final EntityManager em, final TypedQuery<T> query, final long offset, final long size) {
3528
int s = Integer.MAX_VALUE;
3629
if (size < s)
@@ -52,17 +45,18 @@ private <T> T getFirst(final EntityManager em, final TypedQuery<T> query) {
5245
return null;
5346
}
5447

55-
UpsertResult<P> upsert(final EntityManager em, final TypedQuery<P> query, final P entity) {
48+
UpsertResult<P> upsert(final EntityManager em, final TypedQuery<P> query, final P entity,
49+
final Set<Long> tenantIds) {
5650
boolean wasInserted = false;
5751
boolean wasUpdated = false;
5852
P e = getFirst(em, query);
5953
if (e == null) {
60-
e = coreDao.create(em, entity);
54+
e = coreDao.create(em, entity, tenantIds);
6155
wasInserted = true;
6256
} else {
6357
entity.setId(e.getId());
6458
entity.setCreatedOn(e.getCreatedOn());
65-
e = update(em, entity);
59+
e = coreDao.update(em, entity, tenantIds);
6660
wasUpdated = true;
6761
}
6862
return UpsertResult.<P>builder().wasInserted(wasInserted).wasUpdated(wasUpdated).jpa(e).build();

0 commit comments

Comments
 (0)