Skip to content

Commit 91d7cca

Browse files
authored
Merge pull request #27 from palantir/apastrana/pythonjavasdk
Auth features java sdk
2 parents 74922d4 + 923f91c commit 91d7cca

File tree

9 files changed

+311
-1
lines changed

9 files changed

+311
-1
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,38 @@ public class App {
110110
- When the application launches, the compute module is started by invoking the `.start()` method.
111111
- This initiates the module and makes the registered functions, like `hello`, available for execution.
112112
---
113+
### 4. Auth and Credentials
114+
To obtain an auth token for interacting with Foundry resources in Pipeline mode use the following function:
113115

114-
### 4. Build and Deploy with Docker
116+
```java
117+
import com.palantir.computemodules.auth.PipelinesAuth;
118+
119+
String token = PipelinesAuth.retrievePipelineToken();
120+
```
121+
122+
If you have configured your Compute Module (CM) to use Application's permissions, your application will use a service user for permissions instead of relying on the user's permissions. This configuration requires you to obtain the client ID and credentials to grant permission to the service token. This library facilitates this process:
123+
```java
124+
import com.palantir.computemodules.auth.ThirdPartyAuth;
125+
126+
ThirdPartyCredentials credentials = ThirdPartyAuth.retrieveThirdPartyIdAndCreds();
127+
128+
// get a scoped token for your 3pa
129+
String HOSTNAME = "myenvironment.palantirfoundry.com"
130+
String token = ThirdPartyAuth.fetchOAuthToken(HOSTNAME, ["api:datasets-read"]);
131+
```
132+
133+
For usecases where you require the token to automatically refresh after expiry, you can utilize the RefreshingOauthToken class. By default, a token refresh will be triggered after 30 minutes. When using this class, you should ensure to only retrieve and generate tokens through the get_token() function.
134+
```java
135+
import com.palantir.computemodules.auth.RefreshingOauthToken;
136+
137+
RefreshingOauthToken refreshingToken = new RefreshingOauthToken(HOSTNAME, ["api:datasets-read"]);
138+
139+
// Token will automatically refresh when beyond expiry period
140+
String token = refreshingToken.getToken();
141+
```
142+
---
143+
144+
### 5. Build and Deploy with Docker
115145

116146
Containerize your application and then upload the resulting Docker image to Foundry. Once uploaded, you can reference your newly created image in a compute module. Example of Dockerfile:
117147

lib/src/main/java/com/palantir/computemodules/ComputeModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.palantir.computemodules.functions.serde.DefaultDeserializer;
3333
import com.palantir.computemodules.functions.serde.DefaultSerializer;
3434
import com.palantir.logsafe.SafeArg;
35+
import com.palantir.logsafe.Unsafe;
3536
import com.palantir.logsafe.exceptions.SafeRuntimeException;
3637
import com.palantir.logsafe.logger.SafeLogger;
3738
import com.palantir.logsafe.logger.SafeLoggerFactory;
@@ -90,6 +91,7 @@ public void onFailure(Throwable throwable) {
9091
}
9192
}
9293

94+
@Unsafe
9395
private Result execute(ComputeModuleJob job) {
9496
if (functions.containsKey(job.queryType())) {
9597
return functions.get(job.queryType()).run(new Context(job.jobId()), job.query());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.computemodules.auth;
18+
19+
import com.palantir.logsafe.exceptions.SafeRuntimeException;
20+
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
22+
import java.nio.file.Files;
23+
import java.nio.file.Paths;
24+
25+
public final class PipelinesAuth {
26+
private PipelinesAuth() {}
27+
28+
// Produces a bearer token that can be used to make calls to access pipeline resources.
29+
// This is only available in pipeline mode.
30+
public static String retrievePipelineToken() {
31+
String tokenFilePath = System.getenv("BUILD2_TOKEN");
32+
if (tokenFilePath == null || tokenFilePath.isEmpty()) {
33+
throw new SafeRuntimeException(
34+
"Pipeline token not available. Please make sure you are running in Pipeline mode.");
35+
}
36+
try {
37+
return Files.readString(Paths.get(tokenFilePath), StandardCharsets.UTF_8)
38+
.trim();
39+
} catch (IOException e) {
40+
throw new SafeRuntimeException("Failed to read pipeline token from file where it is stored.", e);
41+
}
42+
}
43+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.computemodules.auth;
18+
19+
import com.palantir.logsafe.exceptions.SafeRuntimeException;
20+
import com.palantir.logsafe.logger.SafeLogger;
21+
import com.palantir.logsafe.logger.SafeLoggerFactory;
22+
import java.time.Duration;
23+
import java.time.Instant;
24+
import java.util.List;
25+
26+
public final class RefreshingOauthToken {
27+
28+
private static final Duration DEFAULT_REFRESH_INTERVAL = Duration.ofMinutes(30);
29+
private static final SafeLogger log = SafeLoggerFactory.get(RefreshingOauthToken.class);
30+
31+
private final String hostname;
32+
private final List<String> scope;
33+
private final Duration refreshInterval;
34+
35+
private String token = "";
36+
private volatile Instant lastRefreshTime = Instant.EPOCH;
37+
38+
public RefreshingOauthToken(String hostname, List<String> scope, Duration refreshInterval) {
39+
this.hostname = hostname;
40+
this.scope = scope;
41+
this.refreshInterval = refreshInterval;
42+
}
43+
44+
public RefreshingOauthToken(String hostname, List<String> scope) {
45+
this(hostname, scope, DEFAULT_REFRESH_INTERVAL);
46+
}
47+
48+
public String getToken() {
49+
if (this.token.isEmpty()
50+
|| Duration.between(lastRefreshTime, Instant.now()).compareTo(refreshInterval) > 0) {
51+
String newToken = fetchToken();
52+
if (newToken.isEmpty()) {
53+
throw new SafeRuntimeException("Failed to refresh token");
54+
}
55+
56+
this.token = newToken;
57+
lastRefreshTime = Instant.now();
58+
}
59+
return this.token;
60+
}
61+
62+
private String fetchToken() {
63+
return ThirdPartyAuth.fetchOAuthToken(this.hostname, this.scope);
64+
}
65+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.palantir.computemodules.auth;
17+
18+
import com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.palantir.computemodules.ssl.SslUtils;
21+
import com.palantir.logsafe.exceptions.SafeRuntimeException;
22+
import java.io.IOException;
23+
import java.net.URI;
24+
import java.net.URLEncoder;
25+
import java.net.http.HttpClient;
26+
import java.net.http.HttpRequest;
27+
import java.net.http.HttpResponse;
28+
import java.nio.charset.StandardCharsets;
29+
import java.time.Duration;
30+
import java.util.List;
31+
import java.util.Map;
32+
import javax.net.ssl.SSLContext;
33+
34+
public final class ThirdPartyAuth {
35+
private static final String HTTPS = "https://";
36+
private static final String OAUTH_TOKEN_ENDPOINT = "/multipass/api/oauth2/token";
37+
38+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
39+
40+
private ThirdPartyAuth() {}
41+
42+
public static ThirdPartyCredentials retrieveThirdPartyIdAndCreds() {
43+
String clientId = System.getenv("CLIENT_ID");
44+
String clientSecret = System.getenv("CLIENT_SECRET");
45+
if (clientId.isEmpty() || clientSecret.isEmpty()) {
46+
throw new SafeRuntimeException(
47+
"One or both of required environment variables not found: CLIENT_ID, CLIENT_SECRET. "
48+
+ "Make sure that your CM has Application permissions enabled.");
49+
}
50+
return new ThirdPartyCredentials(clientId, clientSecret);
51+
}
52+
53+
public static String fetchOAuthToken(String hostname, List<String> scope) {
54+
ThirdPartyCredentials credentials = retrieveThirdPartyIdAndCreds();
55+
try {
56+
String formData = "grant_type=" + URLEncoder.encode("client_credentials", StandardCharsets.UTF_8)
57+
+ "&client_id=" + URLEncoder.encode(credentials.clientId(), StandardCharsets.UTF_8)
58+
+ "&client_secret="
59+
+ URLEncoder.encode(credentials.clientSecret(), StandardCharsets.UTF_8)
60+
+ "&scope=" + URLEncoder.encode(String.join(" ", scope), StandardCharsets.UTF_8);
61+
62+
String url = HTTPS + hostname + OAUTH_TOKEN_ENDPOINT;
63+
64+
SSLContext sslContext = SslUtils.createSslContext(System.getenv("DEFAULT_CA_PATH"));
65+
66+
HttpClient client = HttpClient.newBuilder().sslContext(sslContext).build();
67+
HttpRequest request = HttpRequest.newBuilder()
68+
.uri(URI.create(url))
69+
.header("Content-Type", "application/x-www-form-urlencoded")
70+
.POST(HttpRequest.BodyPublishers.ofString(formData))
71+
.timeout(Duration.ofSeconds(10))
72+
.build();
73+
74+
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
75+
76+
if (response.statusCode() == 200) {
77+
Map<String, Object> tokenData =
78+
OBJECT_MAPPER.readValue(response.body(), new TypeReference<Map<String, Object>>() {});
79+
80+
if (tokenData != null && tokenData.containsKey("access_token")) {
81+
return tokenData.get("access_token").toString();
82+
}
83+
}
84+
} catch (InterruptedException e) {
85+
throw new SafeRuntimeException("Interrupted while fetching OAuth token", e);
86+
} catch (IOException e) {
87+
throw new SafeRuntimeException("IOException while fetching OAuth token", e);
88+
}
89+
return "";
90+
}
91+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.computemodules.auth;
18+
19+
public record ThirdPartyCredentials(String clientId, String clientSecret) {}

lib/src/main/java/com/palantir/computemodules/functions/FunctionRunner.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.palantir.computemodules.functions.results.Result;
2121
import com.palantir.computemodules.functions.serde.Deserializer;
2222
import com.palantir.computemodules.functions.serde.Serializer;
23+
import com.palantir.logsafe.Unsafe;
2324
import java.io.InputStream;
2425

2526
public final class FunctionRunner<I, O> {
@@ -42,6 +43,7 @@ public FunctionRunner(
4243
this.serializer = serializer;
4344
}
4445

46+
@Unsafe
4547
public Result run(Context context, Object input) {
4648
I deserializedInput = deserializer.deserialize(input, inputType);
4749
try {

lib/src/main/java/com/palantir/computemodules/functions/results/Result.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515
*/
1616
package com.palantir.computemodules.functions.results;
1717

18+
import com.palantir.logsafe.Unsafe;
19+
20+
@Unsafe
1821
public sealed interface Result permits Ok, Failed {}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.palantir.computemodules.ssl;
18+
19+
import java.io.BufferedInputStream;
20+
import java.io.FileInputStream;
21+
import java.io.InputStream;
22+
import java.security.KeyStore;
23+
import java.security.cert.Certificate;
24+
import java.security.cert.CertificateFactory;
25+
import javax.net.ssl.SSLContext;
26+
import javax.net.ssl.TrustManagerFactory;
27+
28+
public final class SslUtils {
29+
private SslUtils() {}
30+
31+
public static SSLContext createSslContext(String caPath) {
32+
try {
33+
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
34+
ks.load(null, null);
35+
36+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
37+
try (InputStream is = new FileInputStream(caPath);
38+
BufferedInputStream bis = new BufferedInputStream(is)) {
39+
int certIndex = 0;
40+
while (bis.available() > 0) {
41+
Certificate cert = cf.generateCertificate(bis);
42+
ks.setCertificateEntry("alias-" + certIndex, cert);
43+
certIndex++;
44+
}
45+
}
46+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
47+
tmf.init(ks);
48+
SSLContext sslContext = SSLContext.getInstance("TLS");
49+
sslContext.init(null, tmf.getTrustManagers(), null);
50+
return sslContext;
51+
} catch (Exception e) {
52+
throw new RuntimeException(e);
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)