Skip to content

Commit 1d3375a

Browse files
API Key Management Support (#430)
* Add API key management APIs * Add auth layer handling for API Key
1 parent 5405357 commit 1d3375a

File tree

17 files changed

+368
-17
lines changed

17 files changed

+368
-17
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.lowcoder.domain.user.model;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import javax.annotation.Nullable;
7+
import java.util.function.Function;
8+
9+
@Getter
10+
@Setter
11+
public class APIKey {
12+
13+
private String id;
14+
private String name;
15+
private String description;
16+
private String token;
17+
18+
public APIKey(@Nullable String id, String name, String description, String token) {
19+
this.id = id;
20+
this.name = name;
21+
this.description = description;
22+
this.token = token;
23+
}
24+
25+
public void doEncrypt(Function<String, String> encryptFunc) {
26+
this.token = encryptFunc.apply(token);
27+
}
28+
29+
public void doDecrypt(Function<String, String> decryptFunc) {
30+
this.token = decryptFunc.apply(token);
31+
}
32+
33+
}

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/model/User.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import static com.google.common.base.Suppliers.memoize;
44
import static org.lowcoder.infra.util.AssetUtils.toAssetPath;
55

6-
import java.util.HashMap;
7-
import java.util.HashSet;
8-
import java.util.Map;
9-
import java.util.Set;
6+
import java.util.*;
107
import java.util.function.Supplier;
118

9+
import com.fasterxml.jackson.core.type.TypeReference;
1210
import org.apache.commons.collections4.SetUtils;
1311
import org.apache.commons.lang3.StringUtils;
12+
import org.lowcoder.domain.mongodb.AfterMongodbRead;
13+
import org.lowcoder.domain.mongodb.BeforeMongodbWrite;
14+
import org.lowcoder.domain.mongodb.MongodbInterceptorContext;
15+
import org.lowcoder.sdk.config.SerializeConfig;
1416
import org.lowcoder.sdk.models.HasIdAndAuditing;
17+
import org.lowcoder.sdk.util.JsonUtils;
1518
import org.springframework.data.annotation.Transient;
1619
import org.springframework.data.mongodb.core.mapping.Document;
1720

@@ -29,7 +32,7 @@
2932
@ToString
3033
@Document
3134
@JsonIgnoreProperties(ignoreUnknown = true)
32-
public class User extends HasIdAndAuditing {
35+
public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterMongodbRead {
3336

3437
private static final OrgTransformedUserInfo EMPTY_TRANSFORMED_USER_INFO = new OrgTransformedUserInfo();
3538

@@ -52,6 +55,16 @@ public class User extends HasIdAndAuditing {
5255

5356
private Set<Connection> connections;
5457

58+
@Setter
59+
@Getter
60+
@Transient
61+
private List<APIKey> apiKeysList = new ArrayList<>();
62+
63+
/**
64+
* Only used for mongodb (de)serialization
65+
*/
66+
private List<Object> apiKeys = new ArrayList<>();
67+
5568
@Transient
5669
@JsonIgnore
5770
private Supplier<String> avatarUrl = memoize(() -> StringUtils.isNotBlank(avatar) ? toAssetPath(avatar) : tpAvatarLink);
@@ -109,4 +122,18 @@ public void markAsDeleted() {
109122
.forEach(connection -> connection.setSource(
110123
connection.getSource() + "(User deleted at " + System.currentTimeMillis() / 1000 + ")"));
111124
}
125+
126+
@Override
127+
public void beforeMongodbWrite(MongodbInterceptorContext context) {
128+
this.apiKeysList.forEach(apiKey -> apiKey.doEncrypt(s -> context.encryptionService().encryptString(s)));
129+
apiKeys = JsonUtils.fromJsonSafely(JsonUtils.toJsonSafely(apiKeysList, SerializeConfig.JsonViews.Internal.class), new TypeReference<>() {
130+
}, new ArrayList<>());
131+
}
132+
133+
@Override
134+
public void afterMongodbRead(MongodbInterceptorContext context) {
135+
this.apiKeysList = JsonUtils.fromJsonSafely(JsonUtils.toJson(apiKeys), new TypeReference<>() {
136+
}, new ArrayList<>());
137+
this.apiKeysList.forEach(authConfig -> authConfig.doDecrypt(s -> context.encryptionService().decryptString(s)));
138+
}
112139
}

server/api-service/lowcoder-plugins/sqlBasedPlugin/src/main/java/org/lowcoder/plugin/sql/SqlBasedConnector.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ public Set<String> validateConfig(T connectionConfig) {
107107
invalids.add("HOST_WITH_COLON");
108108
}
109109

110-
if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) {
111-
invalids.add("INVALID_HOST");
112-
}
110+
// if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) {
111+
// invalids.add("INVALID_HOST");
112+
// }
113113

114114
if (StringUtils.isBlank(connectionConfig.getDatabase())) {
115115
invalids.add("DATABASE_NAME_EMPTY");

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/AuthProperties.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class AuthProperties {
2727
private Email email = new Email();
2828
private Oauth2Simple google = new Oauth2Simple();
2929
private Oauth2Simple github = new Oauth2Simple();
30+
private ApiKey apiKey = new ApiKey();
3031

3132
@Getter
3233
@Setter
@@ -53,6 +54,12 @@ public static class Oauth2Simple extends AuthWay {
5354
private String clientSecret;
5455
}
5556

57+
@Setter
58+
@Getter
59+
public static class ApiKey {
60+
private String secret;
61+
}
62+
5663
/**
5764
* For saas mode, such as app.lowcoder.cloud
5865
*/

server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/util/CookieHelper.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ public String getCookieToken(ServerWebExchange exchange) {
5353
return getCookieValue(exchange, getCookieName(), "");
5454
}
5555

56-
@Nullable
57-
public String getJWT(ServerWebExchange exchange) {
58-
return getCookieValue(exchange, "JWT", null);
59-
}
60-
6156
public String getCookieValue(ServerWebExchange exchange, String cookieName, String defaultValue) {
6257
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
6358
return ofNullable(cookies.getFirst(cookieName))

server/api-service/lowcoder-server/pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,24 @@
184184
<version>5.9.3</version>
185185
<scope>test</scope>
186186
</dependency>
187+
<dependency>
188+
<groupId>io.jsonwebtoken</groupId>
189+
<artifactId>jjwt-api</artifactId>
190+
<version>0.11.5</version>
191+
<scope>compile</scope>
192+
</dependency>
193+
<dependency>
194+
<groupId>io.jsonwebtoken</groupId>
195+
<artifactId>jjwt-jackson</artifactId>
196+
<version>0.11.5</version>
197+
<scope>compile</scope>
198+
</dependency>
199+
<dependency>
200+
<groupId>io.jsonwebtoken</groupId>
201+
<artifactId>jjwt-impl</artifactId>
202+
<version>0.11.5</version>
203+
<scope>runtime</scope>
204+
</dependency>
187205

188206
</dependencies>
189207

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/AuthenticationController.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
import java.util.List;
44

5+
import org.lowcoder.api.authentication.dto.APIKeyRequest;
56
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
67
import org.lowcoder.api.authentication.service.AuthenticationApiService;
78
import org.lowcoder.api.framework.view.ResponseView;
89
import org.lowcoder.api.home.SessionUserService;
910
import org.lowcoder.api.usermanagement.UserController;
1011
import org.lowcoder.api.usermanagement.UserController.UpdatePasswordRequest;
12+
import org.lowcoder.api.usermanagement.view.APIKeyVO;
1113
import org.lowcoder.api.util.BusinessEventPublisher;
1214
import org.lowcoder.domain.authentication.FindAuthConfig;
15+
import org.lowcoder.domain.user.model.APIKey;
1316
import org.lowcoder.infra.constant.NewUrl;
1417
import org.lowcoder.sdk.auth.AbstractAuthConfig;
1518
import org.lowcoder.sdk.config.SerializeConfig.JsonViews;
@@ -104,6 +107,26 @@ public Mono<ResponseView<List<AbstractAuthConfig>>> getAllConfigs() {
104107
.map(ResponseView::success);
105108
}
106109

110+
// ----------- API Key Management ----------------
111+
@PostMapping("/api-key")
112+
public Mono<ResponseView<APIKeyVO>> createAPIKey(@RequestBody APIKeyRequest apiKeyRequest) {
113+
return authenticationApiService.createAPIKey(apiKeyRequest)
114+
.map(ResponseView::success);
115+
}
116+
117+
@DeleteMapping("/api-key/{id}")
118+
public Mono<ResponseView<Void>> deleteAPIKey(@PathVariable("id") String id) {
119+
return authenticationApiService.deleteAPIKey(id)
120+
.thenReturn(ResponseView.success(null));
121+
}
122+
123+
@GetMapping("/api-keys")
124+
public Mono<ResponseView<List<APIKey>>> getAllAPIKeys() {
125+
return authenticationApiService.findAPIKeys()
126+
.collectList()
127+
.map(ResponseView::success);
128+
}
129+
107130
/**
108131
* @param loginId phone number or email for now.
109132
* @param register register or login
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.lowcoder.api.authentication.dto;
2+
3+
import org.apache.commons.collections4.MapUtils;
4+
import org.apache.commons.lang3.ObjectUtils;
5+
6+
import java.util.HashMap;
7+
8+
import static org.lowcoder.sdk.util.IDUtils.generate;
9+
10+
public class APIKeyRequest extends HashMap<String, Object> {
11+
12+
public String getId() {
13+
return ObjectUtils.firstNonNull(getString("id"), generate());
14+
}
15+
16+
public String getName() {
17+
return getString("name");
18+
}
19+
20+
public String getDescription() {
21+
return getString("description");
22+
}
23+
24+
public String getString(String key) {
25+
return MapUtils.getString(this, key);
26+
}
27+
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package org.lowcoder.api.authentication.service;
22

3+
import org.lowcoder.api.authentication.dto.APIKeyRequest;
34
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
5+
import org.lowcoder.api.usermanagement.view.APIKeyVO;
46
import org.lowcoder.domain.authentication.FindAuthConfig;
7+
import org.lowcoder.domain.user.model.APIKey;
58
import org.lowcoder.domain.user.model.AuthUser;
69
import org.springframework.web.server.ServerWebExchange;
710
import reactor.core.publisher.Flux;
@@ -20,4 +23,10 @@ public interface AuthenticationApiService {
2023
Mono<Boolean> disableAuthConfig(String authId, boolean delete);
2124

2225
Flux<FindAuthConfig> findAuthConfigs(boolean enableOnly);
26+
27+
Mono<APIKeyVO> createAPIKey(APIKeyRequest apiKeyRequest);
28+
29+
Mono<Void> deleteAPIKey(String authId);
30+
31+
Flux<APIKey> findAPIKeys();
2332
}

server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/authentication/service/AuthenticationApiServiceImpl.java

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@
33
import lombok.extern.slf4j.Slf4j;
44
import org.apache.commons.collections4.CollectionUtils;
55
import org.apache.commons.lang3.StringUtils;
6+
import org.apache.commons.lang3.tuple.Pair;
7+
import org.lowcoder.api.authentication.dto.APIKeyRequest;
68
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
79
import org.lowcoder.api.authentication.request.AuthRequestFactory;
810
import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext;
911
import org.lowcoder.api.authentication.service.factory.AuthConfigFactory;
1012
import org.lowcoder.api.authentication.util.AuthenticationUtils;
13+
import org.lowcoder.api.authentication.util.JWTUtils;
1114
import org.lowcoder.api.home.SessionUserService;
1215
import org.lowcoder.api.usermanagement.InvitationApiService;
1316
import org.lowcoder.api.usermanagement.OrgApiService;
1417
import org.lowcoder.api.usermanagement.UserApiService;
18+
import org.lowcoder.api.usermanagement.view.APIKeyVO;
1519
import org.lowcoder.api.util.BusinessEventPublisher;
1620
import org.lowcoder.domain.authentication.AuthenticationService;
1721
import org.lowcoder.domain.authentication.FindAuthConfig;
@@ -22,10 +26,7 @@
2226
import org.lowcoder.domain.organization.model.OrganizationDomain;
2327
import org.lowcoder.domain.organization.service.OrgMemberService;
2428
import org.lowcoder.domain.organization.service.OrganizationService;
25-
import org.lowcoder.domain.user.model.AuthUser;
26-
import org.lowcoder.domain.user.model.Connection;
27-
import org.lowcoder.domain.user.model.ConnectionAuthToken;
28-
import org.lowcoder.domain.user.model.User;
29+
import org.lowcoder.domain.user.model.*;
2930
import org.lowcoder.domain.user.service.UserService;
3031
import org.lowcoder.sdk.auth.AbstractAuthConfig;
3132
import org.lowcoder.sdk.exception.BizError;
@@ -81,6 +82,9 @@ public class AuthenticationApiServiceImpl implements AuthenticationApiService {
8182
@Autowired
8283
private OrgMemberService orgMemberService;
8384

85+
@Autowired
86+
private JWTUtils jwtUtils;
87+
8488
@Override
8589
public Mono<AuthUser> authenticateByForm(String loginId, String password, String source, boolean register, String authId) {
8690
return authenticate(authId, source, new FormAuthRequestContext(loginId, password, register));
@@ -262,6 +266,51 @@ public Flux<FindAuthConfig> findAuthConfigs(boolean enableOnly) {
262266
.flatMapMany(orgMember -> authenticationService.findAllAuthConfigs(orgMember.getOrgId(),false));
263267
}
264268

269+
@Override
270+
public Mono<APIKeyVO> createAPIKey(APIKeyRequest apiKeyRequest) {
271+
return sessionUserService.getVisitor()
272+
.map(user -> {
273+
String token = jwtUtils.createToken(user);
274+
APIKey apiKey = new APIKey(apiKeyRequest.getId(), apiKeyRequest.getName(), apiKeyRequest.getDescription(), token);
275+
addAPIKey(user, apiKey);
276+
return Pair.of(token, user);
277+
})
278+
.flatMap(pair -> userService.update(pair.getRight().getId(), pair.getRight()).thenReturn(pair.getKey()))
279+
.map(APIKeyVO::from);
280+
}
281+
282+
private void addAPIKey(User user, APIKey newApiKey) {
283+
Map<String, APIKey> apiKeyMap = user.getApiKeysList()
284+
.stream()
285+
.collect(Collectors.toMap(APIKey::getId, Function.identity()));
286+
apiKeyMap.put(newApiKey.getId(), newApiKey);
287+
user.setApiKeysList(new ArrayList<>(apiKeyMap.values()));
288+
}
289+
290+
@Override
291+
public Mono<Void> deleteAPIKey(String apiKeyId) {
292+
return sessionUserService.getVisitor()
293+
.doOnNext(user -> deleteAPIKey(user, apiKeyId))
294+
.flatMap(user -> userService.update(user.getId(), user))
295+
.then();
296+
}
297+
298+
private void deleteAPIKey(User user, String apiKeyId) {
299+
List<APIKey> apiKeys = Optional.of(user)
300+
.map(User::getApiKeysList)
301+
.orElse(Collections.emptyList());
302+
apiKeys.removeIf(apiKey -> Objects.equals(apiKey.getId(), apiKeyId));
303+
user.setApiKeysList(apiKeys);
304+
}
305+
306+
@Override
307+
public Flux<APIKey> findAPIKeys() {
308+
return sessionUserService.getVisitor()
309+
.flatMapIterable(user ->
310+
new ArrayList<>(user.getApiKeysList())
311+
);
312+
}
313+
265314

266315
private Mono<Void> removeTokensByAuthId(String authId) {
267316
return sessionUserService.getVisitorOrgMemberCache()

0 commit comments

Comments
 (0)