Skip to content

Commit bf849e1

Browse files
authored
demos: Creating demo for Multi-ACL Account (#611)
Demo for Multi-ACL Account
1 parent aa1ff7c commit bf849e1

File tree

14 files changed

+514
-0
lines changed

14 files changed

+514
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# 🔐 Multi-ACL Account Demo (Redis OM Spring)
2+
3+
This demo illustrates how to use **Redis ACLs** in a Spring Boot application with **Redis OM Spring** to separate read and write responsibilities across different Redis users.
4+
5+
## 🧪 What It Tests
6+
7+
- `userA` can **read and write** data.
8+
- `userB` can **only read**.
9+
- Attempts by `userB` to write throw a `NOPERM` error.
10+
11+
## 🔧 Redis ACL Configuration (Testcontainers)
12+
13+
```conf
14+
user default off on >redispass allcommands allkeys
15+
16+
user userA on >passwordA +@all allkeys
17+
user userB on >passwordB +@read +ping +ft.create allkeys
18+
```
19+
20+
## 🐳 Running the Demo
21+
22+
This demo uses Testcontainers with Redis Open Source 8 and a custom ACL config. No external Redis setup is required.
23+
24+
To run the integration test:
25+
26+
./gradlew :demos:roms-multi-acl-account:test
27+
28+
29+
## Notes
30+
- Each Redis user is wired with a distinct JedisConnectionFactory, RedisModulesClient, and CustomRedisKeyValueTemplate.
31+
- Read and write repositories are configured using @EnableRedisDocumentRepositories pointing to the appropriate templates.
32+
- RedisSearch indexes must be created using a user with +ft.create permission.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
plugins {
2+
id 'java'
3+
id 'org.springframework.boot'
4+
id 'io.spring.dependency-management'
5+
}
6+
7+
java {
8+
toolchain {
9+
languageVersion = JavaLanguageVersion.of(21)
10+
}
11+
}
12+
13+
// Don't publish this module
14+
tasks.matching { it.name.startsWith('publish') }.configureEach {
15+
enabled = false
16+
}
17+
18+
repositories {
19+
mavenLocal()
20+
mavenCentral()
21+
maven {
22+
name = 'Spring Milestones'
23+
url = 'https://repo.spring.io/milestone'
24+
}
25+
maven {
26+
name = 'Spring Snapshots'
27+
url = 'https://repo.spring.io/snapshot'
28+
}
29+
}
30+
31+
dependencies {
32+
implementation project(':redis-om-spring')
33+
34+
// Important for RedisOM annotation processing!
35+
annotationProcessor project(':redis-om-spring')
36+
testAnnotationProcessor project(':redis-om-spring')
37+
38+
// Lombok
39+
compileOnly 'org.projectlombok:lombok'
40+
annotationProcessor 'org.projectlombok:lombok'
41+
testCompileOnly 'org.projectlombok:lombok'
42+
testAnnotationProcessor 'org.projectlombok:lombok'
43+
44+
// Test dependencies
45+
testImplementation 'org.springframework.boot:spring-boot-starter-test'
46+
testImplementation "com.redis:testcontainers-redis:${testcontainersRedisVersion}"
47+
testImplementation "org.testcontainers:junit-jupiter"
48+
}
49+
50+
// Use -parameters flag for Spring
51+
tasks.withType(JavaCompile).configureEach {
52+
options.compilerArgs << '-parameters'
53+
}
54+
55+
test {
56+
useJUnitPlatform()
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.redis.romsmultiaclaccount;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class RomsMultiAclAccountApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(RomsMultiAclAccountApplication.class, args);
11+
}
12+
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.redis.romsmultiaclaccount.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
5+
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
6+
7+
@Configuration
8+
@EnableRedisDocumentRepositories(
9+
basePackages = { "com.redis.romsmultiaclaccount.repository.read", "com.redis.romsmultiaclaccount.model" },
10+
keyValueTemplateRef = "readKeyValueTemplate", redisTemplateRef = "readRedisOperations"
11+
)
12+
class ReadRepoConfig {
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package com.redis.romsmultiaclaccount.config;
2+
3+
import java.time.Duration;
4+
5+
import org.apache.commons.logging.Log;
6+
import org.apache.commons.logging.LogFactory;
7+
import org.springframework.beans.factory.annotation.Qualifier;
8+
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
9+
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
10+
import org.springframework.boot.autoconfigure.thread.Threading;
11+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.context.annotation.Primary;
15+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
16+
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
17+
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
18+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
19+
import org.springframework.data.redis.core.RedisOperations;
20+
import org.springframework.data.redis.core.StringRedisTemplate;
21+
import org.springframework.data.redis.core.mapping.RedisMappingContext;
22+
import org.springframework.lang.Nullable;
23+
24+
import com.google.gson.GsonBuilder;
25+
import com.redis.om.spring.CustomRedisKeyValueTemplate;
26+
import com.redis.om.spring.RedisJSONKeyValueAdapter;
27+
import com.redis.om.spring.RedisOMProperties;
28+
import com.redis.om.spring.client.RedisModulesClient;
29+
import com.redis.om.spring.indexing.RediSearchIndexer;
30+
import com.redis.om.spring.ops.RedisModulesOperations;
31+
import com.redis.om.spring.vectorize.Embedder;
32+
33+
@Configuration
34+
@EnableConfigurationProperties(
35+
{ RedisProperties.class }
36+
)
37+
public class RedisConnectionFactoryConfig {
38+
39+
private static final Log logger = LogFactory.getLog(RedisConnectionFactoryConfig.class);
40+
41+
private final RedisProperties redisProperties;
42+
43+
public RedisConnectionFactoryConfig(RedisProperties redisProperties) {
44+
this.redisProperties = redisProperties;
45+
}
46+
47+
@Bean(
48+
name = "writeJedisConnectionFactory"
49+
)
50+
@ConditionalOnThreading(
51+
Threading.PLATFORM
52+
)
53+
public JedisConnectionFactory writeJedisConnectionFactory() {
54+
return createJedisConnectionFactory("userA", "passwordA");
55+
}
56+
57+
@Bean(
58+
name = "writeJedisConnectionFactoryVirtual"
59+
)
60+
@ConditionalOnThreading(
61+
Threading.VIRTUAL
62+
)
63+
public JedisConnectionFactory writeJedisConnectionFactoryVirtual() {
64+
JedisConnectionFactory factory = createJedisConnectionFactory("userA", "passwordA");
65+
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("redis-write-");
66+
executor.setVirtualThreads(true);
67+
factory.setExecutor(executor);
68+
return factory;
69+
}
70+
71+
@Primary
72+
@Bean(
73+
name = "readJedisConnectionFactory"
74+
)
75+
public JedisConnectionFactory readJedisConnectionFactory() {
76+
return createJedisConnectionFactory("userB", "passwordB");
77+
}
78+
79+
private JedisConnectionFactory createJedisConnectionFactory(String username, String password) {
80+
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties
81+
.getPort());
82+
config.setDatabase(redisProperties.getDatabase());
83+
config.setUsername(username);
84+
config.setPassword(password);
85+
86+
JedisClientConfiguration clientConfig = JedisClientConfiguration.builder().connectTimeout(Duration.ofMillis(
87+
redisProperties.getTimeout().toMillis())).build();
88+
89+
return new JedisConnectionFactory(config, clientConfig);
90+
}
91+
92+
@Bean(
93+
name = "writeRedisModulesClient"
94+
)
95+
public RedisModulesClient writeRedisModulesClient(@Qualifier(
96+
"writeJedisConnectionFactory"
97+
) JedisConnectionFactory factory, @Qualifier(
98+
"omGsonBuilder"
99+
) GsonBuilder builder) {
100+
return new RedisModulesClient(factory, builder);
101+
}
102+
103+
@Bean(
104+
name = "readRedisModulesClient"
105+
)
106+
public RedisModulesClient readRedisModulesClient(@Qualifier(
107+
"readJedisConnectionFactory"
108+
) JedisConnectionFactory factory, @Qualifier(
109+
"omGsonBuilder"
110+
) GsonBuilder builder) {
111+
return new RedisModulesClient(factory, builder);
112+
}
113+
114+
@Bean(
115+
name = "writeRedisModulesOperations"
116+
)
117+
public RedisModulesOperations<?> writeRedisModulesOperations(@Qualifier(
118+
"writeRedisModulesClient"
119+
) RedisModulesClient client, @Qualifier(
120+
"writeJedisConnectionFactory"
121+
) JedisConnectionFactory factory, @Qualifier(
122+
"omGsonBuilder"
123+
) GsonBuilder builder) {
124+
return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder);
125+
}
126+
127+
@Bean(
128+
name = "readRedisModulesOperations"
129+
)
130+
public RedisModulesOperations<?> readRedisModulesOperations(@Qualifier(
131+
"readRedisModulesClient"
132+
) RedisModulesClient client, @Qualifier(
133+
"readJedisConnectionFactory"
134+
) JedisConnectionFactory factory, @Qualifier(
135+
"omGsonBuilder"
136+
) GsonBuilder builder) {
137+
return new RedisModulesOperations<>(client, new StringRedisTemplate(factory), builder);
138+
}
139+
140+
@Bean(
141+
name = "writeRedisOperations"
142+
)
143+
public RedisOperations<String, String> writeRedisOperations(@Qualifier(
144+
"writeJedisConnectionFactory"
145+
) JedisConnectionFactory factory) {
146+
return new StringRedisTemplate(factory);
147+
}
148+
149+
@Bean(
150+
name = "readRedisOperations"
151+
)
152+
public RedisOperations<String, String> readRedisOperations(@Qualifier(
153+
"readJedisConnectionFactory"
154+
) JedisConnectionFactory factory) {
155+
return new StringRedisTemplate(factory);
156+
}
157+
158+
@Bean(
159+
name = "writeKeyValueTemplate"
160+
)
161+
public CustomRedisKeyValueTemplate getWriteRedisJSONKeyValueTemplate(@Qualifier(
162+
"writeRedisOperations"
163+
) RedisOperations<?, ?> redisOps, @Qualifier(
164+
"writeRedisModulesOperations"
165+
) RedisModulesOperations<?> redisModulesOperations, @Qualifier(
166+
"redisEnhancedMappingContext"
167+
) RedisMappingContext mappingContext, RediSearchIndexer indexer, @Qualifier(
168+
"omGsonBuilder"
169+
) GsonBuilder gsonBuilder, RedisOMProperties properties, @Nullable @Qualifier(
170+
"featureExtractor"
171+
) Embedder embedder) {
172+
return new CustomRedisKeyValueTemplate(new RedisJSONKeyValueAdapter(redisOps, redisModulesOperations,
173+
mappingContext, indexer, gsonBuilder, embedder, properties), mappingContext);
174+
}
175+
176+
@Bean(
177+
name = "readKeyValueTemplate"
178+
)
179+
public CustomRedisKeyValueTemplate getReadRedisJSONKeyValueTemplate(@Qualifier(
180+
"readRedisOperations"
181+
) RedisOperations<?, ?> redisOps, @Qualifier(
182+
"readRedisModulesOperations"
183+
) RedisModulesOperations<?> redisModulesOperations, @Qualifier(
184+
"redisEnhancedMappingContext"
185+
) RedisMappingContext mappingContext, RediSearchIndexer indexer, @Qualifier(
186+
"omGsonBuilder"
187+
) GsonBuilder gsonBuilder, RedisOMProperties properties, @Nullable @Qualifier(
188+
"featureExtractor"
189+
) Embedder embedder) {
190+
return new CustomRedisKeyValueTemplate(new RedisJSONKeyValueAdapter(redisOps, redisModulesOperations,
191+
mappingContext, indexer, gsonBuilder, embedder, properties), mappingContext);
192+
}
193+
194+
@Primary
195+
@Bean(
196+
name = "redisModulesOperations"
197+
)
198+
public RedisModulesOperations<?> redisModulesOperationsAlias(@Qualifier(
199+
"readRedisModulesOperations"
200+
) RedisModulesOperations<?> readOps) {
201+
return readOps;
202+
}
203+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.redis.romsmultiaclaccount.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
5+
import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;
6+
7+
@Configuration
8+
@EnableRedisDocumentRepositories(
9+
basePackages = { "com.redis.romsmultiaclaccount.repository.write", "com.redis.romsmultiaclaccount.model" },
10+
keyValueTemplateRef = "writeKeyValueTemplate", redisTemplateRef = "writeRedisOperations"
11+
)
12+
class WriteRepoConfig {
13+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.redis.romsmultiaclaccount.model;
2+
3+
import org.springframework.data.annotation.Id;
4+
5+
import com.redis.om.spring.annotations.Document;
6+
import com.redis.om.spring.annotations.Searchable;
7+
8+
@Document
9+
public class Customer {
10+
@Id
11+
private String id;
12+
13+
@Searchable
14+
private String name;
15+
16+
@Searchable
17+
private String email;
18+
19+
public Customer() {
20+
}
21+
22+
public Customer(String id, String name, String email) {
23+
this.id = id;
24+
this.name = name;
25+
this.email = email;
26+
}
27+
28+
public Customer(String name, String email) {
29+
this.name = name;
30+
this.email = email;
31+
}
32+
33+
public String getId() {
34+
return id;
35+
}
36+
37+
public void setId(String id) {
38+
this.id = id;
39+
}
40+
41+
public String getName() {
42+
return name;
43+
}
44+
45+
public void setName(String name) {
46+
this.name = name;
47+
}
48+
49+
public String getEmail() {
50+
return email;
51+
}
52+
53+
public void setEmail(String email) {
54+
this.email = email;
55+
}
56+
}

0 commit comments

Comments
 (0)