Skip to content

Commit 4c8d734

Browse files
authored
JCL-298: accessgrants e2e tests (#372)
1 parent dc00830 commit 4c8d734

File tree

9 files changed

+558
-7
lines changed

9 files changed

+558
-7
lines changed

integration/base/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
</properties>
2020

2121
<dependencies>
22+
<dependency>
23+
<groupId>com.inrupt.client</groupId>
24+
<artifactId>inrupt-client-accessgrant</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
2227
<dependency>
2328
<groupId>com.inrupt.client</groupId>
2429
<artifactId>inrupt-client-api</artifactId>
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* Copyright 2023 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client.integration.base;
22+
23+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
import static org.junit.jupiter.api.Assertions.assertThrows;
26+
27+
import com.inrupt.client.Request;
28+
import com.inrupt.client.Response;
29+
import com.inrupt.client.accessgrant.AccessGrant;
30+
import com.inrupt.client.accessgrant.AccessGrantClient;
31+
import com.inrupt.client.accessgrant.AccessGrantSession;
32+
import com.inrupt.client.auth.Credential;
33+
import com.inrupt.client.auth.Session;
34+
import com.inrupt.client.openid.OpenIdException;
35+
import com.inrupt.client.openid.OpenIdSession;
36+
import com.inrupt.client.solid.SolidClientException;
37+
import com.inrupt.client.solid.SolidNonRDFSource;
38+
import com.inrupt.client.solid.SolidSyncClient;
39+
import com.inrupt.client.util.URIBuilder;
40+
import com.inrupt.client.webid.WebIdProfile;
41+
42+
import java.io.ByteArrayInputStream;
43+
import java.io.InputStream;
44+
import java.net.URI;
45+
import java.nio.charset.Charset;
46+
import java.time.Instant;
47+
import java.util.Arrays;
48+
import java.util.HashSet;
49+
import java.util.Optional;
50+
import java.util.Set;
51+
import java.util.UUID;
52+
import java.util.stream.Stream;
53+
54+
import org.eclipse.microprofile.config.Config;
55+
import org.eclipse.microprofile.config.ConfigProvider;
56+
import org.junit.jupiter.api.AfterAll;
57+
import org.junit.jupiter.api.BeforeAll;
58+
import org.junit.jupiter.api.DisplayName;
59+
import org.junit.jupiter.params.ParameterizedTest;
60+
import org.junit.jupiter.params.provider.Arguments;
61+
import org.junit.jupiter.params.provider.MethodSource;
62+
import org.slf4j.Logger;
63+
import org.slf4j.LoggerFactory;
64+
65+
public class AccessGrantScenarios {
66+
67+
private static final Logger LOGGER = LoggerFactory.getLogger(AccessGrantScenarios.class);
68+
69+
private static MockSolidServer mockHttpServer;
70+
private static MockOpenIDProvider identityProviderServer;
71+
private static MockUMAAuthorizationServer authServer;
72+
private static MockWebIdService webIdService;
73+
private static MockAccessGrantServer accessGrantServer;
74+
75+
private static String podUrl;
76+
private static String issuer;
77+
private static String webidUrl;
78+
private static final String MOCK_USERNAME = "someuser";
79+
80+
private static final Config config = ConfigProvider.getConfig();
81+
82+
private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class);
83+
private static final String CLIENT_SECRET = config.getValue("inrupt.test.client-secret", String.class);
84+
private static final String AUTH_METHOD = config
85+
.getOptionalValue("inrupt.test.auth-method", String.class)
86+
.orElse("client_secret_basic");
87+
private static String VC_PROVIDER;
88+
private static final String PRIVATE_RESOURCE_PATH = config
89+
.getOptionalValue("inrupt.test.private-resource-path", String.class)
90+
.orElse("private");
91+
92+
private static final URI ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant");
93+
private static final URI ACCESS_REQUEST = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessRequest");
94+
private static URI testContainerURI;
95+
private static String sharedFileName = "sharedFile.txt";
96+
private static URI sharedFileURI;
97+
private static Session session;
98+
99+
@BeforeAll
100+
static void setup() {
101+
authServer = new MockUMAAuthorizationServer();
102+
authServer.start();
103+
104+
mockHttpServer = new MockSolidServer(authServer.getMockServerUrl());
105+
mockHttpServer.start();
106+
107+
identityProviderServer = new MockOpenIDProvider(MOCK_USERNAME);
108+
identityProviderServer.start();
109+
110+
webIdService = new MockWebIdService(
111+
mockHttpServer.getMockServerUrl(),
112+
identityProviderServer.getMockServerUrl(),
113+
MOCK_USERNAME);
114+
webIdService.start();
115+
116+
webidUrl = config
117+
.getOptionalValue("inrupt.test.webid", String.class)
118+
.orElse(URIBuilder.newBuilder(URI.create(webIdService.getMockServerUrl()))
119+
.path(MOCK_USERNAME)
120+
.build()
121+
.toString());
122+
123+
State.PRIVATE_RESOURCE_PATH = PRIVATE_RESOURCE_PATH;
124+
State.WEBID = URI.create(webidUrl);
125+
SolidSyncClient client = SolidSyncClient.getClient();
126+
try (final WebIdProfile profile = client.read(URI.create(webidUrl), WebIdProfile.class)) {
127+
issuer = profile.getOidcIssuer().iterator().next().toString();
128+
podUrl = profile.getStorage().iterator().next().toString();
129+
}
130+
if (!podUrl.endsWith(Utils.FOLDER_SEPARATOR)) {
131+
podUrl += Utils.FOLDER_SEPARATOR;
132+
}
133+
134+
testContainerURI = URIBuilder.newBuilder(URI.create(podUrl))
135+
.path(State.PRIVATE_RESOURCE_PATH)
136+
.path("accessgrant-test-" + UUID.randomUUID())
137+
.build();
138+
139+
sharedFileURI = URIBuilder.newBuilder(URI.create(testContainerURI.toString()))
140+
.path(sharedFileName)
141+
.build();
142+
143+
//create test file in test container
144+
final InputStream is = new ByteArrayInputStream(Charset.forName("UTF-16").encode("Test text").array());
145+
final SolidNonRDFSource testResource = new SolidNonRDFSource(sharedFileURI, Utils.PLAIN_TEXT, is, null);
146+
session = OpenIdSession.ofClientCredentials(
147+
URI.create(issuer), //Client credentials
148+
CLIENT_ID,
149+
CLIENT_SECRET,
150+
AUTH_METHOD);
151+
client = client.session(session);
152+
client.create(testResource);
153+
154+
accessGrantServer = new MockAccessGrantServer(State.WEBID.toString(), sharedFileURI.toString());
155+
accessGrantServer.start();
156+
157+
VC_PROVIDER = config
158+
.getOptionalValue("inrupt.test.vc.provider", String.class)
159+
.orElse(accessGrantServer.getMockServerUrl());
160+
161+
config
162+
.getOptionalValue("inrupt.test.webid", String.class)
163+
.orElse(URIBuilder.newBuilder(URI.create(webIdService.getMockServerUrl()))
164+
.path(MOCK_USERNAME)
165+
.build()
166+
.toString());
167+
168+
LOGGER.info("Integration Test Issuer: [{}]", issuer);
169+
LOGGER.info("Integration Test Pod Host: [{}]", podUrl);
170+
}
171+
@AfterAll
172+
static void teardown() {
173+
//cleanup pod
174+
session = OpenIdSession.ofClientCredentials(
175+
URI.create(issuer), //Client credentials
176+
CLIENT_ID,
177+
CLIENT_SECRET,
178+
AUTH_METHOD);
179+
final SolidSyncClient client = SolidSyncClient.getClient().session(session);
180+
client.send(Request.newBuilder(sharedFileURI).DELETE().build(), Response.BodyHandlers.discarding());
181+
client.send(Request.newBuilder(sharedFileURI.resolve(".")).DELETE().build(),
182+
Response.BodyHandlers.discarding());
183+
184+
mockHttpServer.stop();
185+
identityProviderServer.stop();
186+
authServer.stop();
187+
webIdService.stop();
188+
accessGrantServer.stop();
189+
}
190+
191+
@ParameterizedTest
192+
@MethodSource("provideSessions")
193+
@DisplayName(":accessGrantLifecycle Access Grant issuance lifecycle")
194+
void accessGrantIssuanceLifecycleTest(final Session session) {
195+
LOGGER.info("Integration Test - Access Grant issuance lifecycle");
196+
197+
final AccessGrantClient accessGrantClient = new AccessGrantClient(URI.create(VC_PROVIDER)).session(session);
198+
199+
//Steps
200+
//1. issue & approve access request
201+
final Set<String> modes = new HashSet<>(Arrays.asList("Read"));
202+
final Set<String> purposes = new HashSet<>(Arrays.asList(
203+
"https://some.purpose/not-a-nefarious-one/i-promise",
204+
"https://some.other.purpose/"));
205+
final Instant expiration = Instant.parse("2023-04-03T12:00:00Z");
206+
final AccessGrant grant = accessGrantClient.issue(ACCESS_REQUEST, URI.create(webidUrl),
207+
new HashSet<>(Arrays.asList(sharedFileURI)), modes, purposes, expiration)
208+
.toCompletableFuture().join();
209+
210+
//2. call verify endpoint to verify grant
211+
212+
//3. get access grant from vcProvider - check that they match on proof at least
213+
final URI uri = URIBuilder.newBuilder(URI.create(VC_PROVIDER)).path(grant.getIdentifier().toString()).build();
214+
final AccessGrant grantFromVcProvider = accessGrantClient.fetch(uri).toCompletableFuture().join();
215+
assertEquals(grant.getPurpose(), grantFromVcProvider.getPurpose());
216+
217+
//4. request file with access grant
218+
//unauthorized request test
219+
final SolidSyncClient client = SolidSyncClient.getClient();
220+
final SolidClientException err = assertThrows(SolidClientException.class,
221+
() -> client.read(sharedFileURI, SolidNonRDFSource.class));
222+
assertEquals(Utils.UNAUTHORIZED, err.getStatusCode());
223+
224+
//authorized request test
225+
final Session accessSession = AccessGrantSession.ofAccessGrant(session, grant);
226+
final SolidSyncClient authClient = client.session(accessSession);
227+
228+
try (final SolidNonRDFSource resource = authClient.read(sharedFileURI, SolidNonRDFSource.class)) {
229+
assertEquals(Utils.PLAIN_TEXT, resource.getMetadata().getContentType());
230+
}
231+
232+
//5. revoke access grant
233+
assertDoesNotThrow(accessGrantClient.revoke(grant).toCompletableFuture()::join);
234+
235+
//6. call verify endpoint to check the grant is not valid
236+
}
237+
238+
private static Stream<Arguments> provideSessions() throws SolidClientException {
239+
session = OpenIdSession.ofClientCredentials(
240+
URI.create(issuer), //Client credentials
241+
CLIENT_ID,
242+
CLIENT_SECRET,
243+
AUTH_METHOD);
244+
final Optional<Credential> credential = session.getCredential(OpenIdSession.ID_TOKEN, null);
245+
final var token = credential.map(Credential::getToken)
246+
.orElseThrow(() -> new OpenIdException("We could not get a token"));
247+
return Stream.of(
248+
Arguments.of(OpenIdSession.ofIdToken(token), //OpenId token
249+
Arguments.of(session)));
250+
}
251+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2023 Inrupt Inc.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to use,
7+
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8+
* Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16+
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18+
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
package com.inrupt.client.integration.base;
22+
23+
import static com.github.tomakehurst.wiremock.client.WireMock.*;
24+
import static java.nio.charset.StandardCharsets.UTF_8;
25+
26+
import com.github.tomakehurst.wiremock.WireMockServer;
27+
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
28+
29+
import java.io.IOException;
30+
import java.io.InputStream;
31+
import java.io.UncheckedIOException;
32+
33+
import org.apache.commons.io.IOUtils;
34+
35+
class MockAccessGrantServer {
36+
37+
private final WireMockServer wireMockServer;
38+
private String webId;
39+
private String sharedFile;
40+
41+
public MockAccessGrantServer(final String webId, final String sharedFile) {
42+
this.webId = webId;
43+
this.sharedFile = sharedFile;
44+
wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()
45+
);
46+
}
47+
48+
private void setupMocks() {
49+
wireMockServer.stubFor(get(urlEqualTo(Utils.VC_DISCOVERY_ENDPOINT))
50+
.willReturn(aResponse()
51+
.withStatus(Utils.SUCCESS)
52+
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
53+
.withBody(getResource("/vc-configuration.json", wireMockServer.baseUrl()))));
54+
55+
wireMockServer.stubFor(get(urlEqualTo("/vc-grant"))
56+
.willReturn(aResponse()
57+
.withStatus(Utils.SUCCESS)
58+
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
59+
.withBody(getResource("/vc-grant.json", wireMockServer.baseUrl(),
60+
this.webId, this.sharedFile))));
61+
62+
wireMockServer.stubFor(post(urlEqualTo("/issue"))
63+
.willReturn(aResponse()
64+
.withStatus(Utils.SUCCESS)
65+
.withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON)
66+
.withBody(getResource("/vc-grant.json", wireMockServer.baseUrl(),
67+
this.webId, this.sharedFile))));
68+
69+
wireMockServer.stubFor(post(urlEqualTo("/status"))
70+
.withRequestBody(containing("\"https://accessgrant.example/status/CVAM#2832\""))
71+
.willReturn(aResponse()
72+
.withStatus(Utils.NO_CONTENT)));
73+
74+
}
75+
76+
private String getResource(final String path) {
77+
try (final InputStream res = MockAccessGrantServer.class.getResourceAsStream(path)) {
78+
return new String(IOUtils.toByteArray(res), UTF_8);
79+
} catch (final IOException ex) {
80+
throw new UncheckedIOException("Could not read class resource", ex);
81+
}
82+
}
83+
84+
private String getResource(final String path, final String baseUrl) {
85+
return getResource(path).replace("{{baseUrl}}", baseUrl);
86+
}
87+
88+
private String getResource(final String path, final String baseUrl, final String webId, final String sharedFile) {
89+
return getResource(path).replace("{{baseUrl}}", baseUrl)
90+
.replace("{{webId}}", webId)
91+
.replace("{{sharedFile}}", sharedFile);
92+
}
93+
94+
public void start() {
95+
wireMockServer.start();
96+
setupMocks();
97+
}
98+
99+
public String getMockServerUrl() {
100+
return wireMockServer.baseUrl();
101+
}
102+
103+
public void stop() {
104+
wireMockServer.stop();
105+
}
106+
107+
}
108+

0 commit comments

Comments
 (0)