Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ derby.log

# generated by docs build
*.pyc
/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package io.cdap.cdap.api.security.store;

import io.cdap.cdap.api.annotation.Beta;
import io.cdap.cdap.api.security.store.lease.SecureStoreLease;
import java.io.IOException;
import java.util.Map;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -50,4 +51,29 @@ void put(String namespace, String name, String data, @Nullable String descriptio
* @throws Exception If the specified namespace or name does not exist
*/
void delete(String namespace, String name) throws Exception;

/**
* Attempts to acquire a lease lock on a secret resource.
*
* @param namespace The namespace that this key belongs to
* @param name Name of the element
* @param timeoutMs Lock timeout in milliseconds before lease is considered expired
* @return {@link SecureStoreLease} indicating acquisition success and lock details
* @throws IOException If lock acquisition fails due to underlying storage errors
*/
default SecureStoreLease acquireLease(String namespace, String name, long timeoutMs) throws IOException {
return SecureStoreLease.failed();
}

/**
* Releases an acquired lease lock on a secret resource.
*
* @param namespace The namespace that this key belongs to
* @param name Name of the element
* @param lease {@link SecureStoreLease} to release
* @throws IOException If lock release fails due to underlying storage errors
*/
default void releaseLease(String namespace, String name, SecureStoreLease lease) throws IOException {
// default no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright © 2026 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package io.cdap.cdap.api.security.store.lease;

import java.util.Objects;

/**
* API model representing a distributed lease lock on a secret or credential update in SecureStore.
* <p>
* Note: This is the caller-facing API counterpart to {@code io.cdap.cdap.securestore.spi.lease.SecretLease}
* in the SPI layer, mirroring the {@link io.cdap.cdap.api.security.store.SecureStoreMetadata} vs {@code SecretMetadata}
* architectural pattern in CDAP.
*/
public class SecureStoreLease {
private final boolean acquired;
private final String lockTimestamp;
private final String lockHolder;

private SecureStoreLease(boolean acquired, String lockTimestamp, String lockHolder) {
this.acquired = acquired;
this.lockTimestamp = lockTimestamp;
this.lockHolder = lockHolder;
}

public static SecureStoreLease acquired(String lockTimestamp, String lockHolder) {
return new SecureStoreLease(true, lockTimestamp, lockHolder);
}

public static SecureStoreLease failed() {
return new SecureStoreLease(false, null, null);
}

public boolean isAcquired() {
return acquired;
}

public String getLockTimestamp() {
return lockTimestamp;
}

public String getLockHolder() {
return lockHolder;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SecureStoreLease that = (SecureStoreLease) o;
return acquired == that.acquired
&& Objects.equals(lockTimestamp, that.lockTimestamp)
&& Objects.equals(lockHolder, that.lockHolder);
}

@Override
public int hashCode() {
return Objects.hash(acquired, lockTimestamp, lockHolder);
}

@Override
public String toString() {
return "SecureStoreLease{"
+ "acquired=" + acquired
+ ", lockTimestamp='" + lockTimestamp + '\''
+ ", lockHolder='" + lockHolder + '\''
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.cdap.cdap.api.messaging.TopicAlreadyExistsException;
import io.cdap.cdap.api.messaging.TopicNotFoundException;
import io.cdap.cdap.api.security.store.SecureStoreManager;
import io.cdap.cdap.api.security.store.lease.SecureStoreLease;
import io.cdap.cdap.common.NamespaceNotFoundException;
import io.cdap.cdap.common.namespace.NamespaceQueryAdmin;
import io.cdap.cdap.common.service.Retries;
Expand Down Expand Up @@ -83,6 +84,28 @@ public void delete(String namespace, String name) throws Exception {
Retries.runWithRetries(() -> secureStoreManager.delete(namespace, name), retryStrategy);
}

@Override
public SecureStoreLease acquireLease(String namespace, String name, long timeoutMs) throws IOException {
try {
return Retries.callWithRetries(() -> secureStoreManager.acquireLease(namespace, name, timeoutMs), retryStrategy);
} catch (IOException | RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IOException(e);
}
}

@Override
public void releaseLease(String namespace, String name, SecureStoreLease lease) throws IOException {
try {
Retries.runWithRetries(() -> secureStoreManager.releaseLease(namespace, name, lease), retryStrategy);
} catch (IOException | RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IOException(e);
}
}

@Override
public void createTopic(final String topic) throws TopicAlreadyExistsException, IOException {
if (messagingAdmin == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,42 @@
*/
public class OAuthAccessToken {
private final String accessToken;
private final long expiresAt;
private final String identityUrl;

public OAuthAccessToken(String accessToken) {
this(accessToken, 0L, null);
}

public OAuthAccessToken(String accessToken, long expiresAt) {
this(accessToken, expiresAt, null);
}

public OAuthAccessToken(String accessToken, long expiresAt, String identityUrl) {
this.accessToken = accessToken;
this.expiresAt = expiresAt;
this.identityUrl = identityUrl;
}

public String getAccessToken() {
return accessToken;
}

public long getExpiresAt() {
return expiresAt;
}

public String getIdentityUrl() {
return identityUrl;
}

public boolean isExpired(long safetyBufferMs) {
if (expiresAt <= 0) {
return false;
}
return (expiresAt - System.currentTimeMillis()) <= safetyBufferMs;
}

public static Builder newBuilder() {
return new Builder();
}
Expand All @@ -39,6 +66,8 @@ public static Builder newBuilder() {
*/
public static class Builder {
private String accessToken;
private long expiresAt;
private String identityUrl;

public Builder() {}

Expand All @@ -47,9 +76,19 @@ public Builder withAccessToken(String accessToken) {
return this;
}

public Builder withExpiresAt(long expiresAt) {
this.expiresAt = expiresAt;
return this;
}

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

public OAuthAccessToken build() {
Preconditions.checkNotNull(accessToken, "OAuth access token missing");
return new OAuthAccessToken(accessToken);
return new OAuthAccessToken(accessToken, expiresAt, identityUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,31 @@ public class OAuthProvider {
// Optional string to send as a USER_AGENT header
@Nullable
private final String userAgent;
private final OAuthType oauthType;

public OAuthProvider(String name,
String loginURL,
String tokenRefreshURL,
@Nullable OAuthClientCredentials clientCreds,
@Nullable CredentialEncodingStrategy strategy,
@Nullable String userAgent) {
this(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent, OAuthType.STANDARD);
}

public OAuthProvider(String name,
String loginURL,
String tokenRefreshURL,
@Nullable OAuthClientCredentials clientCreds,
@Nullable CredentialEncodingStrategy strategy,
@Nullable String userAgent,
@Nullable OAuthType oauthType) {
this.name = name;
this.loginURL = loginURL;
this.tokenRefreshURL = tokenRefreshURL;
this.clientCreds = clientCreds;
this.strategy = strategy;
this.userAgent = userAgent;
this.oauthType = oauthType == null ? OAuthType.STANDARD : oauthType;
}

public String getName() {
Expand Down Expand Up @@ -74,13 +86,23 @@ public String getUserAgent() {
return userAgent;
}

public OAuthType getOAuthType() {
return oauthType;
}

public enum CredentialEncodingStrategy {
// (default) Sends client ID & secret as part of the POST request body
FORM_BODY,
// Sends client ID & secret as part of a HTTP Basic Auth header
BASIC_AUTH,
}

public enum OAuthType {
STANDARD,
PKCE,
REFRESH_TOKEN_ROTATION
}

public static Builder newBuilder() {
return new Builder();
}
Expand All @@ -95,6 +117,7 @@ public static class Builder {
private OAuthClientCredentials clientCreds;
private CredentialEncodingStrategy strategy;
private String userAgent;
private OAuthType oauthType;

public Builder() {}

Expand Down Expand Up @@ -128,6 +151,11 @@ public Builder withUserAgent(@Nullable String userAgent) {
return this;
}

public Builder withOAuthType(@Nullable OAuthType oauthType) {
this.oauthType = oauthType;
return this;
}

public OAuthProvider build() {
Preconditions.checkNotNull(name, "OAuth provider name missing");
Preconditions.checkNotNull(loginURL, "Login URL missing");
Expand All @@ -136,7 +164,10 @@ public OAuthProvider build() {
if (strategy == null) {
this.strategy = CredentialEncodingStrategy.FORM_BODY;
}
return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent);
if (oauthType == null) {
this.oauthType = OAuthType.STANDARD;
}
return new OAuthProvider(name, loginURL, tokenRefreshURL, clientCreds, strategy, userAgent, oauthType);
}
}
}
Loading
Loading