Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ skills
plan
skills-lock.json
channel-diff-tasks
test
/test
docs/superpowers/plans
doc
2 changes: 1 addition & 1 deletion CHANNEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class AgentBot {
<dependency>
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<version>2.6.1</version>
<version>2.7.3</version>
</dependency>
```

Expand Down
17 changes: 17 additions & 0 deletions larksuite-oapi/src/main/java/com/lark/oapi/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import com.lark.oapi.service.cardkit.CardkitService;

import com.lark.oapi.service.ext.ExtService;
import com.lark.oapi.core.auth.ClientAssertionProvider;
import com.lark.oapi.core.httpclient.IHttpTransport;
import com.lark.oapi.core.httpclient.OkHttpTransport;
import com.lark.oapi.core.Transport;
Expand Down Expand Up @@ -169,6 +170,7 @@ public class Client {
private CardkitService cardkit;

private ExtService extService;
private com.lark.oapi.core.accesstoken.AccessToken accessToken;

public static Builder newBuilder(String appId, String appSecret) {
return new Builder(appId, appSecret);
Expand All @@ -178,6 +180,10 @@ public ExtService ext() {
return extService;
}

public com.lark.oapi.core.accesstoken.AccessToken accessToken() {
return accessToken;
}

public void setConfig(Config config) {
this.config = config;
}
Expand Down Expand Up @@ -538,6 +544,16 @@ public Builder openBaseUrl(BaseUrlEnum baseUrl) {
return this;
}

public Builder oauthBaseUrl(String oauthBaseUrl) {
config.setOAuthBaseUrl(oauthBaseUrl);
return this;
}

public Builder clientAssertionProvider(ClientAssertionProvider provider) {
config.setClientAssertionProvider(provider);
return this;
}

public Builder tokenCache(ICache cache) {
config.setCache(cache);
return this;
Expand Down Expand Up @@ -585,6 +601,7 @@ public Client build() {
client.setConfig(config);
initCache(config);
initHttpTransport(config);
client.accessToken = new com.lark.oapi.core.accesstoken.AccessToken(config);
client.extService = new ExtService(config);
client.wiki = new WikiService(config);
client.workplace = new WorkplaceService(config);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ static Client createRawClient(LarkChannelOptions options) {
if (options.getSource() != null) {
builder.source(options.getSource());
}
if (options.getClientAssertionProvider() != null) {
builder.clientAssertionProvider(options.getClientAssertionProvider());
}
if (options.getOAuthBaseUrl() != null) {
builder.oauthBaseUrl(options.getOAuthBaseUrl());
}
return builder.build();
}

Expand All @@ -39,6 +45,7 @@ static com.lark.oapi.ws.Client createWebSocketClient(
.eventHandler(eventDispatcher)
.domain(options.getDomain() == null ? BaseUrlEnum.FeiShu.getUrl() : options.getDomain())
.source(options.getSource())
.clientAssertionProvider(options.getClientAssertionProvider())
.onReconnecting(new Runnable() {
@Override
public void run() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lark.oapi.channel.config;

import com.lark.oapi.core.auth.ClientAssertionProvider;
import com.lark.oapi.core.cache.ICache;
import com.lark.oapi.core.httpclient.IHttpTransport;
import com.lark.oapi.core.request.RequestOptions;
Expand Down Expand Up @@ -28,6 +29,8 @@ public class LarkChannelOptions {
private final RequestOptions httpInstance;
private final String source;
private final boolean includeRawInMessage;
private final ClientAssertionProvider clientAssertionProvider;
private final String oauthBaseUrl;

private LarkChannelOptions(Builder builder) {
this.appId = builder.appId;
Expand All @@ -43,6 +46,8 @@ private LarkChannelOptions(Builder builder) {
this.httpInstance = builder.httpInstance;
this.source = builder.source;
this.includeRawInMessage = builder.includeRawInMessage;
this.clientAssertionProvider = builder.clientAssertionProvider;
this.oauthBaseUrl = builder.oauthBaseUrl;
}

public static Builder newBuilder(String appId, String appSecret) {
Expand Down Expand Up @@ -97,6 +102,14 @@ public String getSource() {
return source;
}

public ClientAssertionProvider getClientAssertionProvider() {
return clientAssertionProvider;
}

public String getOAuthBaseUrl() {
return oauthBaseUrl;
}

/**
* Whether normalized events should carry the original Feishu event body.
*
Expand Down Expand Up @@ -131,6 +144,8 @@ public static final class Builder {
private RequestOptions httpInstance;
private String source;
private boolean includeRawInMessage;
private ClientAssertionProvider clientAssertionProvider;
private String oauthBaseUrl;

private Builder(String appId, String appSecret) {
this.appId = appId;
Expand Down Expand Up @@ -191,6 +206,16 @@ public Builder source(String source) {
return this;
}

public Builder clientAssertionProvider(ClientAssertionProvider clientAssertionProvider) {
this.clientAssertionProvider = clientAssertionProvider;
return this;
}

public Builder oauthBaseUrl(String oauthBaseUrl) {
this.oauthBaseUrl = oauthBaseUrl;
return this;
}

/**
* Attach the raw Feishu event body to normalized events. Useful when a
* handler needs fields that the normalizer intentionally drops, such as
Expand Down
19 changes: 19 additions & 0 deletions larksuite-oapi/src/main/java/com/lark/oapi/core/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@


import com.lark.oapi.core.cache.ICache;
import com.lark.oapi.core.auth.ClientAssertionProvider;
import com.lark.oapi.core.enums.AppType;
import com.lark.oapi.core.enums.BaseUrlEnum;
import com.lark.oapi.core.httpclient.IHttpTransport;
Expand All @@ -37,6 +38,8 @@ public class Config {
private IHttpTransport httpTransport;
private boolean logReqAtDebug;
private String source;
private String oauthBaseUrl;
private ClientAssertionProvider clientAssertionProvider;

public Config() {
this.baseUrl = BaseUrlEnum.FeiShu.getUrl();
Expand Down Expand Up @@ -167,4 +170,20 @@ public void setSource(String source) {
this.source = source;
}

public String getOAuthBaseUrl() {
return oauthBaseUrl;
}

public void setOAuthBaseUrl(String oauthBaseUrl) {
this.oauthBaseUrl = oauthBaseUrl;
}

public ClientAssertionProvider getClientAssertionProvider() {
return clientAssertionProvider;
}

public void setClientAssertionProvider(ClientAssertionProvider clientAssertionProvider) {
this.clientAssertionProvider = clientAssertionProvider;
}

}
11 changes: 11 additions & 0 deletions larksuite-oapi/src/main/java/com/lark/oapi/core/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ public interface Constants {
String APP_ACCESS_TOKEN_ISV_URL_PATH = "/open-apis/auth/v3/app_access_token";
String TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH = "/open-apis/auth/v3/tenant_access_token/internal";
String TENANT_ACCESS_TOKEN_ISV_URL_PATH = "/open-apis/auth/v3/tenant_access_token";
String OAUTH_TOKEN_URL_PATH = "/oauth/v3/token";
String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
String GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
String CLIENT_ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
String HEADER_X_TARGET_SERVICE = "X-Target-Service";
int ERR_CODE_CLIENT_ASSERTION_PROVIDER_NOT_CONFIGURED = 7100;
int ERR_CODE_CLIENT_ASSERTION_TOKEN_EMPTY = 7101;
int ERR_CODE_CLIENT_ASSERTION_RETRIEVE_FAILED = 7102;
int ERR_CODE_CLIENT_ASSERTION_MODE_NOT_SUPPORTED = 7103;
int ERR_CODE_APP_SECRET_AND_CLIENT_ASSERTION_EMPTY = 7104;
String APPLY_APP_TICKET_PATH = "/open-apis/auth/v3/app_ticket/resend";
String GET_AUTHEN_ACCESS_TOKEN = "/open-apis/authen/v1/access_token";
String REFRESH_AUTHEN_ACCESS_TOKEN = "/open-apis/authen/v1/refresh_access_token";
Expand Down
104 changes: 98 additions & 6 deletions larksuite-oapi/src/main/java/com/lark/oapi/core/Transport.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import com.lark.oapi.core.exception.AccessTokenNotGivenException;
import com.lark.oapi.core.exception.ClientTimeoutException;
import com.lark.oapi.core.exception.ClientAssertionException;
import com.lark.oapi.core.exception.IllegalAccessTokenTypeException;
import com.lark.oapi.core.exception.ServerTimeoutException;
import com.lark.oapi.core.httpclient.IHttpTransport;
Expand All @@ -23,6 +24,7 @@
import com.lark.oapi.core.request.RequestOptions;
import com.lark.oapi.core.response.RawResponse;
import com.lark.oapi.core.token.AccessTokenType;
import com.lark.oapi.core.enums.AppType;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.core.utils.OKHttps;
import com.lark.oapi.core.utils.Strings;
Expand All @@ -31,15 +33,46 @@

import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

public class Transport {

private static final Logger log = LoggerFactory.getLogger(Transport.class);
private static final ReqTranslator REQ_TRANSLATOR = new ReqTranslator();
private static final String OMITTED = "<omitted>";

private static AccessTokenType determineTokenType(Set<AccessTokenType> accessTokenTypeSet,
RequestOptions requestOptions, boolean disableTokenCache) {
RequestOptions requestOptions, boolean disableTokenCache,
Config config) {
if (config.getClientAssertionProvider() != null) {
validateTokenType(accessTokenTypeSet, requestOptions);

if (Strings.isNotEmpty(requestOptions.getUserAccessToken())
&& accessTokenTypeSet.contains(AccessTokenType.User)) {
return AccessTokenType.User;
}

if (accessTokenTypeSet.contains(AccessTokenType.Tenant)) {
return AccessTokenType.Tenant;
}

if (accessTokenTypeSet.contains(AccessTokenType.App)) {
throw new ClientAssertionException(
Constants.ERR_CODE_CLIENT_ASSERTION_MODE_NOT_SUPPORTED,
"AppAccessToken APIs are not available in ClientAssertion mode");
}

if (accessTokenTypeSet.contains(AccessTokenType.None)) {
return AccessTokenType.None;
}

throw new IllegalAccessTokenTypeException();
}

if (accessTokenTypeSet.contains(AccessTokenType.None)) {
return AccessTokenType.None;
}
Expand Down Expand Up @@ -110,7 +143,21 @@ private static void validate(Config config, RequestOptions requestOptions,
throw new IllegalArgumentException("appId is blank");
}

if (Strings.isEmpty(config.getAppSecret())) {
if (config.getClientAssertionProvider() != null
&& config.getAppType() == AppType.MARKETPLACE) {
throw new ClientAssertionException(
Constants.ERR_CODE_CLIENT_ASSERTION_PROVIDER_NOT_CONFIGURED,
"ClientAssertion mode is not supported for marketplace apps");
}

boolean hasManualAccessToken =
(accessTokenType == AccessTokenType.User && Strings.isNotEmpty(requestOptions.getUserAccessToken()))
|| (accessTokenType == AccessTokenType.Tenant && Strings.isNotEmpty(requestOptions.getTenantAccessToken()))
|| (accessTokenType == AccessTokenType.App && Strings.isNotEmpty(requestOptions.getAppAccessToken()));

if (config.getClientAssertionProvider() == null
&& Strings.isEmpty(config.getAppSecret())
&& !hasManualAccessToken) {
throw new IllegalArgumentException("appSecret is blank");
}

Expand Down Expand Up @@ -176,7 +223,8 @@ public static RawResponse send(Config config
// 确定token类型
AccessTokenType accessTokenType = determineTokenType(accessTokenTypeSet
, requestOptions
, config.isDisableTokenCache());
, config.isDisableTokenCache()
, config);

// 参数校验
validate(config, requestOptions, accessTokenType);
Expand All @@ -203,16 +251,60 @@ private static void logReq(RawRequest req, String httpPath, boolean isUpload) {

if (!isUpload) {
log.debug("req,path:{},header:{},body:{}", httpPath
, Jsons.DEFAULT.toJson(req.getHeaders())
, req.getBody() == null ? "" : Jsons.DEFAULT.toJson(req.getBody()));
, Jsons.DEFAULT.toJson(safeHeaders(req.getHeaders()))
, safeBody(req.getBody()));
} else {
log.debug("req,path:{},header:{}", httpPath, req.getHeaders());
log.debug("req,path:{},header:{}", httpPath, safeHeaders(req.getHeaders()));
}
} catch (Throwable e) {
log.error("logReq error:{}", e);
}
}

private static Map<String, List<String>> safeHeaders(Map<String, List<String>> headers) {
Map<String, List<String>> safeHeaders = new HashMap<>();
if (headers == null) {
return safeHeaders;
}
headers.entrySet().stream().forEach(entry -> {
if (!isSensitiveKey(entry.getKey())) {
safeHeaders.put(entry.getKey(), entry.getValue());
}
});
return safeHeaders;
}

private static String safeBody(Object body) {
if (body == null) {
return "";
}
String json = Jsons.DEFAULT.toJson(body);
return containsSensitiveField(json) ? OMITTED : json;
}

private static boolean containsSensitiveField(String json) {
if (Strings.isEmpty(json)) {
return false;
}
String normalized = json.toLowerCase(Locale.ROOT);
return normalized.contains("\"client_secret\"")
|| normalized.contains("\"clientassertion\"")
|| normalized.contains("\"client_assertion\"")
|| normalized.contains("\"refresh_token\"")
|| normalized.contains("\"access_token\"")
|| normalized.contains("\"tenant_access_token\"")
|| normalized.contains("\"app_access_token\"");
}

private static boolean isSensitiveKey(String key) {
if (Strings.isEmpty(key)) {
return false;
}
String normalized = key.toLowerCase(Locale.ROOT);
return "authorization".equals(normalized)
|| Constants.X_HELPDESK_AUTHORIZATION.toLowerCase(Locale.ROOT).equals(normalized);
}

private static RawResponse doSend(Config config, String httpMethod, String httpPath,
AccessTokenType accessTokenType, Object req, RequestOptions requestOptions) throws Exception {
Exception error = null;
Expand Down
Loading