Skip to content

API Key Management Support #430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.lowcoder.domain.user.model;

import lombok.Getter;
import lombok.Setter;

import javax.annotation.Nullable;
import java.util.function.Function;

@Getter
@Setter
public class APIKey {

private String id;
private String name;
private String description;
private String token;

public APIKey(@Nullable String id, String name, String description, String token) {
this.id = id;
this.name = name;
this.description = description;
this.token = token;
}

public void doEncrypt(Function<String, String> encryptFunc) {
this.token = encryptFunc.apply(token);
}

public void doDecrypt(Function<String, String> decryptFunc) {
this.token = decryptFunc.apply(token);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
import static com.google.common.base.Suppliers.memoize;
import static org.lowcoder.infra.util.AssetUtils.toAssetPath;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.function.Supplier;

import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.StringUtils;
import org.lowcoder.domain.mongodb.AfterMongodbRead;
import org.lowcoder.domain.mongodb.BeforeMongodbWrite;
import org.lowcoder.domain.mongodb.MongodbInterceptorContext;
import org.lowcoder.sdk.config.SerializeConfig;
import org.lowcoder.sdk.models.HasIdAndAuditing;
import org.lowcoder.sdk.util.JsonUtils;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

Expand All @@ -29,7 +32,7 @@
@ToString
@Document
@JsonIgnoreProperties(ignoreUnknown = true)
public class User extends HasIdAndAuditing {
public class User extends HasIdAndAuditing implements BeforeMongodbWrite, AfterMongodbRead {

private static final OrgTransformedUserInfo EMPTY_TRANSFORMED_USER_INFO = new OrgTransformedUserInfo();

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

private Set<Connection> connections;

@Setter
@Getter
@Transient
private List<APIKey> apiKeysList = new ArrayList<>();

/**
* Only used for mongodb (de)serialization
*/
private List<Object> apiKeys = new ArrayList<>();

@Transient
@JsonIgnore
private Supplier<String> avatarUrl = memoize(() -> StringUtils.isNotBlank(avatar) ? toAssetPath(avatar) : tpAvatarLink);
Expand Down Expand Up @@ -109,4 +122,18 @@ public void markAsDeleted() {
.forEach(connection -> connection.setSource(
connection.getSource() + "(User deleted at " + System.currentTimeMillis() / 1000 + ")"));
}

@Override
public void beforeMongodbWrite(MongodbInterceptorContext context) {
this.apiKeysList.forEach(apiKey -> apiKey.doEncrypt(s -> context.encryptionService().encryptString(s)));
apiKeys = JsonUtils.fromJsonSafely(JsonUtils.toJsonSafely(apiKeysList, SerializeConfig.JsonViews.Internal.class), new TypeReference<>() {
}, new ArrayList<>());
}

@Override
public void afterMongodbRead(MongodbInterceptorContext context) {
this.apiKeysList = JsonUtils.fromJsonSafely(JsonUtils.toJson(apiKeys), new TypeReference<>() {
}, new ArrayList<>());
this.apiKeysList.forEach(authConfig -> authConfig.doDecrypt(s -> context.encryptionService().decryptString(s)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ public Set<String> validateConfig(T connectionConfig) {
invalids.add("HOST_WITH_COLON");
}

if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) {
invalids.add("INVALID_HOST");
}
// if (StringUtils.equalsIgnoreCase(host, "localhost") || StringUtils.equals(host, "127.0.0.1")) {
// invalids.add("INVALID_HOST");
// }

if (StringUtils.isBlank(connectionConfig.getDatabase())) {
invalids.add("DATABASE_NAME_EMPTY");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class AuthProperties {
private Email email = new Email();
private Oauth2Simple google = new Oauth2Simple();
private Oauth2Simple github = new Oauth2Simple();
private ApiKey apiKey = new ApiKey();

@Getter
@Setter
Expand All @@ -53,6 +54,12 @@ public static class Oauth2Simple extends AuthWay {
private String clientSecret;
}

@Setter
@Getter
public static class ApiKey {
private String secret;
}

/**
* For saas mode, such as app.lowcoder.cloud
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,6 @@ public String getCookieToken(ServerWebExchange exchange) {
return getCookieValue(exchange, getCookieName(), "");
}

@Nullable
public String getJWT(ServerWebExchange exchange) {
return getCookieValue(exchange, "JWT", null);
}

public String getCookieValue(ServerWebExchange exchange, String cookieName, String defaultValue) {
MultiValueMap<String, HttpCookie> cookies = exchange.getRequest().getCookies();
return ofNullable(cookies.getFirst(cookieName))
Expand Down
18 changes: 18 additions & 0 deletions server/api-service/lowcoder-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,24 @@
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import java.util.List;

import org.lowcoder.api.authentication.dto.APIKeyRequest;
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
import org.lowcoder.api.authentication.service.AuthenticationApiService;
import org.lowcoder.api.framework.view.ResponseView;
import org.lowcoder.api.home.SessionUserService;
import org.lowcoder.api.usermanagement.UserController;
import org.lowcoder.api.usermanagement.UserController.UpdatePasswordRequest;
import org.lowcoder.api.usermanagement.view.APIKeyVO;
import org.lowcoder.api.util.BusinessEventPublisher;
import org.lowcoder.domain.authentication.FindAuthConfig;
import org.lowcoder.domain.user.model.APIKey;
import org.lowcoder.infra.constant.NewUrl;
import org.lowcoder.sdk.auth.AbstractAuthConfig;
import org.lowcoder.sdk.config.SerializeConfig.JsonViews;
Expand Down Expand Up @@ -104,6 +107,26 @@ public Mono<ResponseView<List<AbstractAuthConfig>>> getAllConfigs() {
.map(ResponseView::success);
}

// ----------- API Key Management ----------------
@PostMapping("/api-key")
public Mono<ResponseView<APIKeyVO>> createAPIKey(@RequestBody APIKeyRequest apiKeyRequest) {
return authenticationApiService.createAPIKey(apiKeyRequest)
.map(ResponseView::success);
}

@DeleteMapping("/api-key/{id}")
public Mono<ResponseView<Void>> deleteAPIKey(@PathVariable("id") String id) {
return authenticationApiService.deleteAPIKey(id)
.thenReturn(ResponseView.success(null));
}

@GetMapping("/api-keys")
public Mono<ResponseView<List<APIKey>>> getAllAPIKeys() {
return authenticationApiService.findAPIKeys()
.collectList()
.map(ResponseView::success);
}

/**
* @param loginId phone number or email for now.
* @param register register or login
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.lowcoder.api.authentication.dto;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.ObjectUtils;

import java.util.HashMap;

import static org.lowcoder.sdk.util.IDUtils.generate;

public class APIKeyRequest extends HashMap<String, Object> {

public String getId() {
return ObjectUtils.firstNonNull(getString("id"), generate());
}

public String getName() {
return getString("name");
}

public String getDescription() {
return getString("description");
}

public String getString(String key) {
return MapUtils.getString(this, key);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.lowcoder.api.authentication.service;

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

Flux<FindAuthConfig> findAuthConfigs(boolean enableOnly);

Mono<APIKeyVO> createAPIKey(APIKeyRequest apiKeyRequest);

Mono<Void> deleteAPIKey(String authId);

Flux<APIKey> findAPIKeys();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.lowcoder.api.authentication.dto.APIKeyRequest;
import org.lowcoder.api.authentication.dto.AuthConfigRequest;
import org.lowcoder.api.authentication.request.AuthRequestFactory;
import org.lowcoder.api.authentication.request.oauth2.OAuth2RequestContext;
import org.lowcoder.api.authentication.service.factory.AuthConfigFactory;
import org.lowcoder.api.authentication.util.AuthenticationUtils;
import org.lowcoder.api.authentication.util.JWTUtils;
import org.lowcoder.api.home.SessionUserService;
import org.lowcoder.api.usermanagement.InvitationApiService;
import org.lowcoder.api.usermanagement.OrgApiService;
import org.lowcoder.api.usermanagement.UserApiService;
import org.lowcoder.api.usermanagement.view.APIKeyVO;
import org.lowcoder.api.util.BusinessEventPublisher;
import org.lowcoder.domain.authentication.AuthenticationService;
import org.lowcoder.domain.authentication.FindAuthConfig;
Expand All @@ -22,10 +26,7 @@
import org.lowcoder.domain.organization.model.OrganizationDomain;
import org.lowcoder.domain.organization.service.OrgMemberService;
import org.lowcoder.domain.organization.service.OrganizationService;
import org.lowcoder.domain.user.model.AuthUser;
import org.lowcoder.domain.user.model.Connection;
import org.lowcoder.domain.user.model.ConnectionAuthToken;
import org.lowcoder.domain.user.model.User;
import org.lowcoder.domain.user.model.*;
import org.lowcoder.domain.user.service.UserService;
import org.lowcoder.sdk.auth.AbstractAuthConfig;
import org.lowcoder.sdk.exception.BizError;
Expand Down Expand Up @@ -81,6 +82,9 @@ public class AuthenticationApiServiceImpl implements AuthenticationApiService {
@Autowired
private OrgMemberService orgMemberService;

@Autowired
private JWTUtils jwtUtils;

@Override
public Mono<AuthUser> authenticateByForm(String loginId, String password, String source, boolean register, String authId) {
return authenticate(authId, source, new FormAuthRequestContext(loginId, password, register));
Expand Down Expand Up @@ -262,6 +266,51 @@ public Flux<FindAuthConfig> findAuthConfigs(boolean enableOnly) {
.flatMapMany(orgMember -> authenticationService.findAllAuthConfigs(orgMember.getOrgId(),false));
}

@Override
public Mono<APIKeyVO> createAPIKey(APIKeyRequest apiKeyRequest) {
return sessionUserService.getVisitor()
.map(user -> {
String token = jwtUtils.createToken(user);
APIKey apiKey = new APIKey(apiKeyRequest.getId(), apiKeyRequest.getName(), apiKeyRequest.getDescription(), token);
addAPIKey(user, apiKey);
return Pair.of(token, user);
})
.flatMap(pair -> userService.update(pair.getRight().getId(), pair.getRight()).thenReturn(pair.getKey()))
.map(APIKeyVO::from);
}

private void addAPIKey(User user, APIKey newApiKey) {
Map<String, APIKey> apiKeyMap = user.getApiKeysList()
.stream()
.collect(Collectors.toMap(APIKey::getId, Function.identity()));
apiKeyMap.put(newApiKey.getId(), newApiKey);
user.setApiKeysList(new ArrayList<>(apiKeyMap.values()));
}

@Override
public Mono<Void> deleteAPIKey(String apiKeyId) {
return sessionUserService.getVisitor()
.doOnNext(user -> deleteAPIKey(user, apiKeyId))
.flatMap(user -> userService.update(user.getId(), user))
.then();
}

private void deleteAPIKey(User user, String apiKeyId) {
List<APIKey> apiKeys = Optional.of(user)
.map(User::getApiKeysList)
.orElse(Collections.emptyList());
apiKeys.removeIf(apiKey -> Objects.equals(apiKey.getId(), apiKeyId));
user.setApiKeysList(apiKeys);
}

@Override
public Flux<APIKey> findAPIKeys() {
return sessionUserService.getVisitor()
.flatMapIterable(user ->
new ArrayList<>(user.getApiKeysList())
);
}


private Mono<Void> removeTokensByAuthId(String authId) {
return sessionUserService.getVisitorOrgMemberCache()
Expand Down
Loading