Skip to content

Commit 28513e6

Browse files
mkleenestrantalisttschampel
authored
feat: add code to create services for SDK (#35)
Also add `bom` for `junit` to lock down version --------- Co-authored-by: Sean Trantalis <strantalis@virtru.com> Co-authored-by: Timothy Tschampel <ttschampel@virtru.com>
1 parent 4a8e507 commit 28513e6

File tree

11 files changed

+677
-82
lines changed

11 files changed

+677
-82
lines changed

.github/workflows/checks.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ on:
1313
types:
1414
- checks_requested
1515

16+
permissions:
17+
contents: read
18+
1619
jobs:
1720
pr:
1821
name: Validate PR title

pom.xml

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<maven.compiler.source>11</maven.compiler.source>
1616
<maven.compiler.target>11</maven.compiler.target>
1717
<log4j.version>2.20.0</log4j.version>
18-
<grpc.version>1.57.2</grpc.version>
18+
<grpc.version>1.63.0</grpc.version>
1919
<protobuf.version>3.25.3</protobuf.version>
2020
</properties>
2121
<modules>
@@ -25,9 +25,18 @@
2525
<dependencyManagement>
2626
<dependencies>
2727
<dependency>
28-
<groupId>io.opentdf.platform</groupId>
29-
<artifactId>platform-client</artifactId>
30-
<version>0.1.0-SNAPSHOT</version><!-- {x-version-update:java-sdk:current} -->
28+
<groupId>io.grpc</groupId>
29+
<artifactId>grpc-bom</artifactId>
30+
<version>${grpc.version}</version>
31+
<type>pom</type>
32+
<scope>import</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.junit</groupId>
36+
<artifactId>junit-bom</artifactId>
37+
<version>5.10.1</version>
38+
<type>pom</type>
39+
<scope>import</scope>
3140
</dependency>
3241
<dependency>
3342
<groupId>org.apache.logging.log4j</groupId>
@@ -50,12 +59,6 @@
5059
<version>1.18.30</version>
5160
<scope>provided</scope>
5261
</dependency>
53-
<dependency>
54-
<groupId>org.junit.jupiter</groupId>
55-
<artifactId>junit-jupiter-engine</artifactId>
56-
<version>5.9.2</version>
57-
<scope>test</scope>
58-
</dependency>
5962
<dependency>
6063
<groupId>org.mockito</groupId>
6164
<artifactId>mockito-core</artifactId>
@@ -68,13 +71,6 @@
6871
<version>5.2.0</version>
6972
<scope>test</scope>
7073
</dependency>
71-
<dependency>
72-
<groupId>org.junit.jupiter</groupId>
73-
<artifactId>junit-jupiter-api</artifactId>
74-
<version>5.9.2</version>
75-
<scope>test</scope>
76-
</dependency>
77-
7874
<dependency>
7975
<groupId>org.apache.maven.plugin-tools</groupId>
8076
<artifactId>maven-plugin-annotations</artifactId>

protocol/pom.xml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,10 @@
3232
<dependency>
3333
<groupId>io.grpc</groupId>
3434
<artifactId>grpc-protobuf</artifactId>
35-
<version>${grpc.version}</version>
36-
</dependency>
35+
</dependency>
3736
<dependency>
3837
<groupId>io.grpc</groupId>
3938
<artifactId>grpc-stub</artifactId>
40-
<version>${grpc.version}</version>
4139
</dependency>
4240
</dependencies>
4341
<build>

sdk/pom.xml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
55
<modelVersion>4.0.0</modelVersion>
6-
<groupId>io.opentdf.platform</groupId>
76
<name>sdk</name>
87
<artifactId>sdk</artifactId>
98
<parent>
@@ -20,7 +19,43 @@
2019
</dependency>
2120
<dependency>
2221
<groupId>org.junit.jupiter</groupId>
23-
<artifactId>junit-jupiter-engine</artifactId>
22+
<artifactId>junit-jupiter</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>com.nimbusds</groupId>
26+
<artifactId>oauth2-oidc-sdk</artifactId>
27+
<version>11.10.1</version>
28+
</dependency>
29+
<dependency>
30+
<groupId>io.grpc</groupId>
31+
<artifactId>grpc-netty-shaded</artifactId>
32+
<scope>runtime</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>io.grpc</groupId>
36+
<artifactId>grpc-protobuf</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>io.grpc</groupId>
40+
<artifactId>grpc-stub</artifactId>
41+
</dependency>
42+
<dependency> <!-- necessary for Java 9+ -->
43+
<groupId>org.apache.tomcat</groupId>
44+
<artifactId>annotations-api</artifactId>
45+
<version>6.0.53</version>
46+
<scope>provided</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.assertj</groupId>
50+
<artifactId>assertj-core</artifactId>
51+
<version>3.8.0</version>
52+
<scope>test</scope>
53+
</dependency>
54+
<dependency>
55+
<groupId>com.squareup.okhttp3</groupId>
56+
<artifactId>mockwebserver</artifactId>
57+
<version>5.0.0-alpha.14</version>
58+
<scope>test</scope>
2459
</dependency>
2560
</dependencies>
2661
</project>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import com.nimbusds.jose.JOSEException;
4+
import com.nimbusds.jose.JWSAlgorithm;
5+
import com.nimbusds.jose.jwk.RSAKey;
6+
import com.nimbusds.jwt.SignedJWT;
7+
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
8+
import com.nimbusds.oauth2.sdk.ClientCredentialsGrant;
9+
import com.nimbusds.oauth2.sdk.ErrorObject;
10+
import com.nimbusds.oauth2.sdk.TokenRequest;
11+
import com.nimbusds.oauth2.sdk.TokenResponse;
12+
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
13+
import com.nimbusds.oauth2.sdk.dpop.DPoPProofFactory;
14+
import com.nimbusds.oauth2.sdk.dpop.DefaultDPoPProofFactory;
15+
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
16+
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
17+
import com.nimbusds.oauth2.sdk.token.AccessToken;
18+
import com.nimbusds.oauth2.sdk.tokenexchange.TokenExchangeGrant;
19+
import io.grpc.CallOptions;
20+
import io.grpc.Channel;
21+
import io.grpc.ClientCall;
22+
import io.grpc.ClientInterceptor;
23+
import io.grpc.ForwardingClientCall;
24+
import io.grpc.Metadata;
25+
import io.grpc.MethodDescriptor;
26+
27+
import java.net.URI;
28+
import java.net.URISyntaxException;
29+
import java.time.Instant;
30+
31+
/**
32+
* The GRPCAuthInterceptor class is responsible for intercepting client calls before they are sent
33+
* to the server. It adds authentication headers to the requests by fetching and caching access
34+
* tokens.
35+
*/
36+
class GRPCAuthInterceptor implements ClientInterceptor {
37+
private Instant tokenExpiryTime;
38+
private AccessToken token;
39+
private final ClientAuthentication clientAuth;
40+
private final RSAKey rsaKey;
41+
private final URI tokenEndpointURI;
42+
43+
/**
44+
* Constructs a new GRPCAuthInterceptor with the specified client authentication and RSA key.
45+
*
46+
* @param clientAuth the client authentication to be used by the interceptor
47+
* @param rsaKey the RSA key to be used by the interceptor
48+
*/
49+
public GRPCAuthInterceptor(ClientAuthentication clientAuth, RSAKey rsaKey, URI tokenEndpointURI) {
50+
this.clientAuth = clientAuth;
51+
this.rsaKey = rsaKey;
52+
this.tokenEndpointURI = tokenEndpointURI;
53+
}
54+
55+
/**
56+
* Intercepts the client call before it is sent to the server.
57+
*
58+
* @param method The method descriptor for the call.
59+
* @param callOptions The call options for the call.
60+
* @param next The next channel in the channel pipeline.
61+
* @param <ReqT> The type of the request message.
62+
* @param <RespT> The type of the response message.
63+
* @return A client call with the intercepted behavior.
64+
*/
65+
@Override
66+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
67+
CallOptions callOptions, Channel next) {
68+
return new ForwardingClientCall.SimpleForwardingClientCall<>(next.newCall(method, callOptions)) {
69+
@Override
70+
public void start(Listener<RespT> responseListener, Metadata headers) {
71+
// Get the access token
72+
AccessToken t = getToken();
73+
headers.put(Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
74+
"DPoP " + t.getValue());
75+
76+
// Build the DPoP proof for each request
77+
try {
78+
DPoPProofFactory dpopFactory = new DefaultDPoPProofFactory(rsaKey, JWSAlgorithm.RS256);
79+
80+
URI uri = new URI("/" + method.getFullMethodName());
81+
SignedJWT proof = dpopFactory.createDPoPJWT("POST", uri, t);
82+
headers.put(Metadata.Key.of("DPoP", Metadata.ASCII_STRING_MARSHALLER),
83+
proof.serialize());
84+
} catch (URISyntaxException e) {
85+
throw new RuntimeException("Invalid URI syntax for DPoP proof creation", e);
86+
} catch (JOSEException e) {
87+
throw new RuntimeException("Error creating DPoP proof", e);
88+
}
89+
super.start(responseListener, headers);
90+
}
91+
};
92+
}
93+
94+
/**
95+
* Either fetches a new access token or returns the cached access token if it is still valid.
96+
*
97+
* @return The access token.
98+
*/
99+
private synchronized AccessToken getToken() {
100+
try {
101+
// If the token is expired or initially null, get a new token
102+
if (token == null || isTokenExpired()) {
103+
104+
// Construct the client credentials grant
105+
AuthorizationGrant clientGrant = new ClientCredentialsGrant();
106+
107+
// Make the token request
108+
TokenRequest tokenRequest = new TokenRequest(this.tokenEndpointURI,
109+
clientAuth, clientGrant, null);
110+
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
111+
112+
DPoPProofFactory dpopFactory = new DefaultDPoPProofFactory(rsaKey, JWSAlgorithm.RS256);
113+
114+
SignedJWT proof = dpopFactory.createDPoPJWT(httpRequest.getMethod().name(), httpRequest.getURI());
115+
116+
httpRequest.setDPoP(proof);
117+
TokenResponse tokenResponse;
118+
119+
HTTPResponse httpResponse = httpRequest.send();
120+
121+
tokenResponse = TokenResponse.parse(httpResponse);
122+
if (!tokenResponse.indicatesSuccess()) {
123+
ErrorObject error = tokenResponse.toErrorResponse().getErrorObject();
124+
throw new RuntimeException("Token request failed: " + error);
125+
}
126+
127+
this.token = tokenResponse.toSuccessResponse().getTokens().getAccessToken();
128+
// DPoPAccessToken dPoPAccessToken = tokens.getDPoPAccessToken();
129+
130+
131+
if (token.getLifetime() != 0) {
132+
// Need some type of leeway but not sure whats best
133+
this.tokenExpiryTime = Instant.now().plusSeconds(token.getLifetime() / 3);
134+
}
135+
136+
} else {
137+
// If the token is still valid or not initially null, return the cached token
138+
return this.token;
139+
}
140+
141+
} catch (Exception e) {
142+
// TODO Auto-generated catch block
143+
throw new RuntimeException("failed to get token", e);
144+
}
145+
return this.token;
146+
}
147+
148+
/**
149+
* Checks if the token has expired.
150+
*
151+
* @return true if the token has expired, false otherwise.
152+
*/
153+
private boolean isTokenExpired() {
154+
return this.tokenExpiryTime != null && this.tokenExpiryTime.isBefore(Instant.now());
155+
}
156+
}
Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,60 @@
11
package io.opentdf.platform.sdk;
22

3+
import io.grpc.Channel;
34
import io.opentdf.platform.policy.attributes.AttributesServiceGrpc;
5+
import io.opentdf.platform.policy.attributes.AttributesServiceGrpc.AttributesServiceFutureStub;
6+
import io.opentdf.platform.policy.namespaces.NamespaceServiceGrpc;
7+
import io.opentdf.platform.policy.namespaces.NamespaceServiceGrpc.NamespaceServiceFutureStub;
48
import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceGrpc;
9+
import io.opentdf.platform.policy.resourcemapping.ResourceMappingServiceGrpc.ResourceMappingServiceFutureStub;
10+
import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc;
11+
import io.opentdf.platform.policy.subjectmapping.SubjectMappingServiceGrpc.SubjectMappingServiceFutureStub;
512

613
/**
7-
* Interact with OpenTDF platform services and perform TDF data operations with
8-
* this object.
14+
* The SDK class represents a software development kit for interacting with the opentdf platform. It
15+
* provides various services and stubs for making API calls to the opentdf platform.
916
*/
1017
public class SDK {
11-
private final String platformEndpoint;
12-
private AttributesServiceGrpc.AttributesServiceFutureStub attributesServiceFutureStub;
13-
private ResourceMappingServiceGrpc.ResourceMappingServiceFutureStub resourceMappingServiceFutureStub;
14-
15-
public AttributesServiceGrpc.AttributesServiceFutureStub getAttributesServiceFutureStub() {
16-
return attributesServiceFutureStub;
17-
}
18-
19-
public ResourceMappingServiceGrpc.ResourceMappingServiceFutureStub getResourceMappingServiceFutureStub() {
20-
return resourceMappingServiceFutureStub;
21-
}
22-
23-
24-
private SDK(String platformEndpoint) {
25-
this.platformEndpoint = platformEndpoint;
26-
}
27-
28-
public String getPlatformEndpoint() {
29-
return this.platformEndpoint;
30-
}
31-
32-
/**
33-
* Builder pattern for SDK objects
34-
*/
35-
public static class Builder implements Cloneable {
36-
private String platformEndpoint;
37-
38-
public Builder platformEndpoint(String platformEndpoint) {
39-
this.platformEndpoint = platformEndpoint;
40-
return this;
41-
}
42-
43-
public SDK build() {
44-
return new SDK(this.platformEndpoint);
18+
private final Services services;
19+
20+
// TODO: add KAS
21+
public interface Services {
22+
AttributesServiceFutureStub attributes();
23+
NamespaceServiceFutureStub namespaces();
24+
SubjectMappingServiceFutureStub subjectMappings();
25+
ResourceMappingServiceFutureStub resourceMappings();
26+
27+
static Services newServices(Channel channel) {
28+
var attributeService = AttributesServiceGrpc.newFutureStub(channel);
29+
var namespaceService = NamespaceServiceGrpc.newFutureStub(channel);
30+
var subjectMappingService = SubjectMappingServiceGrpc.newFutureStub(channel);
31+
var resourceMappingService = ResourceMappingServiceGrpc.newFutureStub(channel);
32+
33+
return new Services() {
34+
@Override
35+
public AttributesServiceFutureStub attributes() {
36+
return attributeService;
37+
}
38+
39+
@Override
40+
public NamespaceServiceFutureStub namespaces() {
41+
return namespaceService;
42+
}
43+
44+
@Override
45+
public SubjectMappingServiceFutureStub subjectMappings() {
46+
return subjectMappingService;
47+
}
48+
49+
@Override
50+
public ResourceMappingServiceFutureStub resourceMappings() {
51+
return resourceMappingService;
52+
}
53+
};
54+
}
4555
}
4656

47-
@Override public Builder clone() {
48-
try {
49-
return (Builder) super.clone();
50-
} catch (CloneNotSupportedException e) {
51-
throw new RuntimeException(e);
52-
}
57+
public SDK(Services services) {
58+
this.services = services;
5359
}
54-
}
55-
}
60+
}

0 commit comments

Comments
 (0)