Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
package org.eclipse.che.api.factory.server.scm.kubernetes;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.Long.parseLong;
import static org.eclipse.che.api.factory.server.scm.PersonalAccessTokenFetcher.OAUTH_2_PREFIX;
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;

Expand Down Expand Up @@ -75,8 +76,16 @@ public class KubernetesPersonalAccessTokenManager implements PersonalAccessToken
public static final String ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME =
"che.eclipse.org/scm-personal-access-token-name";
public static final String ANNOTATION_SCM_URL = "che.eclipse.org/scm-url";

/** Kubernetes secret annotation key for the token expiration time in seconds. */
public static final String ANNOTATION_SCM_TOKEN_EXPIRES_IN =
"che.eclipse.org/scm-token-expires-in";

public static final String TOKEN_DATA_FIELD = "token";

/** Kubernetes secret data field key for the OAuth refresh token. */
public static final String REFRESH_TOKEN_DATA_FIELD = "refresh-token";

private final KubernetesNamespaceFactory namespaceFactory;
private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
private final ScmPersonalAccessTokenFetcher scmPersonalAccessTokenFetcher;
Expand All @@ -102,34 +111,41 @@ public void store(PersonalAccessToken personalAccessToken)
throws UnsatisfiedScmPreconditionException, ScmConfigurationPersistenceException {
try {
String namespace = getFirstNamespace();
ImmutableMap.Builder<String, String> annotations =
new ImmutableMap.Builder<String, String>()
.put(ANNOTATION_CHE_USERID, personalAccessToken.getCheUserId())
.put(ANNOTATION_SCM_URL, personalAccessToken.getScmProviderUrl())
.put(ANNOTATION_SCM_PROVIDER_NAME, personalAccessToken.getScmProviderName())
.put(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID, personalAccessToken.getScmTokenId())
.put(
ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME, personalAccessToken.getScmTokenName());
// Only OAuth tokens with a known lifetime get the annotation. Storing `0` would be
// indistinguishable from a token that expires immediately, so it is omitted instead.
if (personalAccessToken.getExpiresIn() > 0) {
annotations.put(
ANNOTATION_SCM_TOKEN_EXPIRES_IN, String.valueOf(personalAccessToken.getExpiresIn()));
}
ObjectMeta meta =
new ObjectMetaBuilder()
.withName(NameGenerator.generate(NAME_PATTERN, 5))
.withAnnotations(
new ImmutableMap.Builder<String, String>()
.put(ANNOTATION_CHE_USERID, personalAccessToken.getCheUserId())
.put(ANNOTATION_SCM_URL, personalAccessToken.getScmProviderUrl())
.put(ANNOTATION_SCM_PROVIDER_NAME, personalAccessToken.getScmProviderName())
.put(
ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID,
personalAccessToken.getScmTokenId())
.put(
ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME,
personalAccessToken.getScmTokenName())
.build())
.withAnnotations(annotations.build())
.withLabels(SECRET_LABELS)
.build();

Secret secret =
new SecretBuilder()
.withMetadata(meta)
.withData(
Map.of(
TOKEN_DATA_FIELD,
Base64.getEncoder()
.encodeToString(
personalAccessToken.getToken().getBytes(StandardCharsets.UTF_8))))
.build();
// Kubernetes secrets store data as Base64-encoded values
String tokenEncoded =
Base64.getEncoder()
.encodeToString(personalAccessToken.getToken().getBytes(StandardCharsets.UTF_8));
ImmutableMap.Builder<String, String> data =
new ImmutableMap.Builder<String, String>().put(TOKEN_DATA_FIELD, tokenEncoded);
// Refresh token is absent for PATs and for OAuth providers that don't issue one
String refreshToken = personalAccessToken.getRefreshToken();
if (!isNullOrEmpty(refreshToken)) {
data.put(
REFRESH_TOKEN_DATA_FIELD,
Base64.getEncoder().encodeToString(refreshToken.getBytes(StandardCharsets.UTF_8)));
}
Secret secret = new SecretBuilder().withMetadata(meta).withData(data.build()).build();

cheServerKubernetesClientFactory
.create()
Expand Down Expand Up @@ -263,7 +279,9 @@ private List<PersonalAccessToken> doGetPersonalAccessTokens(
scmUsername.get(),
personalAccessTokenParams.getScmTokenName(),
personalAccessTokenParams.getScmTokenId(),
personalAccessTokenParams.getToken());
personalAccessTokenParams.getToken(),
personalAccessTokenParams.getRefreshToken(),
personalAccessTokenParams.getExpiresIn());
result.add(personalAccessToken);
continue;
}
Expand Down Expand Up @@ -398,10 +416,32 @@ private boolean deleteSecretIfMisconfigured(Secret secret) throws Infrastructure
return false;
}

/** Extracts token parameters from a Kubernetes secret, decoding Base64-encoded data fields. */
private PersonalAccessTokenParams secret2PersonalAccessTokenParams(Secret secret) {
Map<String, String> secretAnnotations = secret.getMetadata().getAnnotations();

String token = new String(Base64.getDecoder().decode(secret.getData().get("token"))).trim();
String refreshTokenData = secret.getData().get(REFRESH_TOKEN_DATA_FIELD);
String expiresInAnnotation = secretAnnotations.get(ANNOTATION_SCM_TOKEN_EXPIRES_IN);

String token =
new String(Base64.getDecoder().decode(secret.getData().get(TOKEN_DATA_FIELD))).trim();
// Refresh token and expiresIn may be absent in PAT secrets, or secrets created before OAuth
// refresh support
String refreshToken =
isNullOrEmpty(refreshTokenData)
? null
: new String(Base64.getDecoder().decode(refreshTokenData)).trim();
long expiresIn = 0;
if (!isNullOrEmpty(expiresInAnnotation)) {
try {
expiresIn = parseLong(expiresInAnnotation.trim());
} catch (NumberFormatException e) {
LOG.warn(
"Invalid '{}' annotation value '{}' in secret '{}'. Treating token as non-expiring.",
ANNOTATION_SCM_TOKEN_EXPIRES_IN,
expiresInAnnotation,
secret.getMetadata().getName());
}
}
String configuredOAuthTokenName =
secretAnnotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME);
String configuredTokenId = secretAnnotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_ID);
Expand All @@ -415,7 +455,9 @@ private PersonalAccessTokenParams secret2PersonalAccessTokenParams(Secret secret
configuredOAuthTokenName,
configuredTokenId,
token,
configuredScmOrganization);
configuredScmOrganization,
refreshToken,
expiresIn);
}

private boolean isSecretMatchesSearchCriteria(
Expand All @@ -426,8 +468,7 @@ private boolean isSecretMatchesSearchCriteria(
Map<String, String> secretAnnotations = secret.getMetadata().getAnnotations();
String configuredScmServerUrl = secretAnnotations.get(ANNOTATION_SCM_URL);
String configuredCheUserId = secretAnnotations.get(ANNOTATION_CHE_USERID);
String configuredOAuthProviderName =
secretAnnotations.get(ANNOTATION_SCM_PERSONAL_ACCESS_TOKEN_NAME);
String configuredOAuthProviderName = secretAnnotations.get(ANNOTATION_SCM_PROVIDER_NAME);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls Have a look

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a leftover fix from the pull request
The main branch is not affected by this leftover as we try to get token by url if we fail to get the token by provider name:

Optional<PersonalAccessToken> tokenOptional =
personalAccessTokenManager.get(cheSubject, oAuthProviderName, null, namespaceName);
if (tokenOptional.isPresent()) {
return fetchGitUserDataWithPersonalAccessToken(tokenOptional.get());
} else {
Optional<PersonalAccessToken> oAuthTokenOptional =
personalAccessTokenManager.get(cheSubject, null, oAuthProviderUrl, namespaceName);

I would like to keep the fix


return (configuredCheUserId.equals(cheUser.getUserId()))
&& (oAuthProviderName == null || oAuthProviderName.equals(configuredOAuthProviderName))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2025 Red Hat, Inc.
* Copyright (c) 2012-2026 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -95,10 +95,13 @@ public void testCreateAndSaveNewPATGitCredential() throws Exception {
"https://bitbucket.com",
"provider",
"cheUser",
null,
"username",
"token-name",
"tid-23434",
"token123");
"token123",
null,
0);

// when
kubernetesGitCredentialManager.createOrReplace(token);
Expand All @@ -120,12 +123,15 @@ public void shouldUseHardcodedUsernameIfScmOrganizationIsDefined() throws Except
PersonalAccessToken token =
new PersonalAccessToken(
"https://bitbucket-server.com:5648",
"provider",
"cheUser",
"cheOrganization",
"username",
"token-name",
"tid-23434",
"token123");
"token123",
null,
0);

Map<String, String> annotations = new HashMap<>(DEFAULT_SECRET_ANNOTATIONS);

Expand Down Expand Up @@ -182,10 +188,13 @@ public void testCreateAndSaveNewOAuthGitCredential() throws Exception {
"https://bitbucket.com",
"provider",
"cheUser",
null,
"username",
"oauth2-token-name",
"tid-23434",
"token123");
"token123",
null,
0);

// when
kubernetesGitCredentialManager.createOrReplace(token);
Expand Down Expand Up @@ -218,10 +227,13 @@ public void testCreateAndSaveNewBitbucketOAuthGitCredential() throws Exception {
"https://bitbucket.com",
"bitbucket",
"cheUser",
null,
"username",
"oauth2-token-name",
"tid-23434",
"token123");
"token123",
null,
0);

// when
kubernetesGitCredentialManager.createOrReplace(token);
Expand All @@ -244,10 +256,13 @@ public void testUpdateTokenInExistingCredential() throws Exception {
"https://bitbucket.com:5648",
"provider",
"cheUser",
null,
"username",
"token-name",
"tid-23434",
"token123");
"token123",
null,
0);

Map<String, String> annotations = new HashMap<>(DEFAULT_SECRET_ANNOTATIONS);

Expand Down
Loading
Loading