Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 implementsBeforeMongodbWrite, 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)

AltStyle によって変換されたページ (->オリジナル) /