Skip to content

Commit

Permalink
Fix #33302: allow to replace DelegateProxies
Browse files Browse the repository at this point in the history
  • Loading branch information
LEDfan committed Jun 11, 2024
1 parent 1bb5f4d commit 2f75900
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public void sendRedirect(HttpServletRequest request, HttpServletResponse respons
http.authorizeHttpRequests(authz -> authz
.requestMatchers(
new MvcRequestMatcher(handlerMappingIntrospector, "/admin"),
new MvcRequestMatcher(handlerMappingIntrospector, "/admin/data"))
new MvcRequestMatcher(handlerMappingIntrospector, "/admin/**"))
.access((authentication, context) -> new AuthorizationDecision(userService.isAdmin(authentication.get())))
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ private String admin(ModelMap map, HttpServletRequest request) {
@RequestMapping(value = "/admin/data", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET)
@ResponseBody
private ResponseEntity<ApiResponse<List<ProxyInfo>>> adminData() {
// TODO rename to /admin/proxy
List<Proxy> proxies = proxyService.getAllProxies();
List<ProxyInfo> proxyInfos = proxies.stream().map(ProxyInfo::new).toList();
return ApiResponse.success(proxyInfos);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* ShinyProxy
*
* Copyright (C) 2016-2024 Open Analytics
*
* ===========================================================================
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Apache License as published by
* The Apache Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Apache License for more details.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/>
*/
package eu.openanalytics.shinyproxy.controllers;

import eu.openanalytics.containerproxy.api.dto.ApiResponse;
import eu.openanalytics.containerproxy.event.RemoveDelegateProxiesEvent;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.inject.Inject;

@Controller
public class DelegateProxyAdminController extends BaseController {

@Inject
private ApplicationEventPublisher applicationEventPublisher;

@Operation(summary = "Stops DelegateProxies. Can only be used by admins. If no parameters are specified, all DelegateProxies (of all specs) are stopped. " +
"DelegateProxies that have claimed seats will be stopped as soon as all seats are released. " +
"New DelegateProxies are automatically created to meet the minimum number of seats.",
tags = "ShinyProxy"
)
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "The DelegateProxies are being stopped.",
content = {
@Content(
mediaType = "application/json",
examples = {
@ExampleObject(value = "{\"status\":\"success\", \"data\": null}")
}
)
}),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400",
description = "Invalid request, no DelegateProxies are being stopped.",
content = {
@Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "Both id and specId are specified, provide only a single parameter.", value = "{\"status\":\"fail\",\"data\":\"Id and specId cannot be specified at the same time\"}"),
}
)
}),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "403",
description = "Forbidden, you are not an admin user.",
content = {
@Content(
mediaType = "application/json",
examples = {@ExampleObject(value = "{\"status\": \"fail\", \"data\": \"forbidden\"}")}
)
}),
})

@RequestMapping(value = "/admin/delegate-proxy", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.DELETE)
@ResponseBody
public ResponseEntity<ApiResponse<Object>> stopDelegateProxies(
@Parameter(description = "If specified stops the DelegateProxy with this id") @RequestParam(required = false) String id,
@Parameter(description = "If specified stops all DelegateProxies of this specId ") @RequestParam(required = false) String specId
) {

if (id != null && specId != null) {
return ApiResponse.fail("Id and specId cannot be specified at the same time");
}

applicationEventPublisher.publishEvent(new RemoveDelegateProxiesEvent(id, specId));

return ApiResponse.success();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public void testProxy() {
resp = apiTestHelper.callWithAuth(apiTestHelper.createRequest("/app_proxy/" + id + "/").addHeader("Accept", "text/html"));
resp.assertHtmlSuccess();
Assertions.assertTrue(resp.body().contains("Welcome to nginx!"));
Assertions.assertTrue(resp.body().endsWith("<script src='/5e89c377af39026486b5a487ad46f0b55d6031aa/js/shiny.iframe.js'></script>"));
Assertions.assertTrue(resp.body().endsWith("<script src='/9f0e4e7085654f8393139ec029b480b1ca8bbe96/js/shiny.iframe.js'></script>"));

// normal sub-path request
resp = apiTestHelper.callWithAuth(apiTestHelper.createRequest("/app_proxy/" + id + "/my-path"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* ShinyProxy
*
* Copyright (C) 2016-2024 Open Analytics
*
* ===========================================================================
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Apache License as published by
* The Apache Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Apache License for more details.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/>
*/
package eu.openanalytics.shinyproxy.test.api;

import eu.openanalytics.containerproxy.test.helpers.ShinyProxyInstance;
import eu.openanalytics.shinyproxy.test.helpers.ApiTestHelper;
import eu.openanalytics.shinyproxy.test.helpers.Response;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;

public class DelegateProxyAdminControllerTest {

private static final ShinyProxyInstance inst = new ShinyProxyInstance("application-test-delegate.yml");
private static final ApiTestHelper apiTestHelper = new ApiTestHelper(inst);
private static final String RANDOM_UUID = "8402e8c3-eaef-4fc7-9f23-9e843739dd0f";

@AfterAll
public static void afterAll() {
inst.close();
}

@Test
public void testWithoutAuth() {
Response resp = apiTestHelper.callWithoutAuth(apiTestHelper.createDeleteRequest("/admin/delegate-proxy"));
resp.assertHtmlAuthenticationRequired();

resp = apiTestHelper.callWithoutAuth(apiTestHelper.createDeleteRequest("/admin/delegate-proxy?id=" + RANDOM_UUID));
resp.assertHtmlAuthenticationRequired();

resp = apiTestHelper.callWithoutAuth(apiTestHelper.createDeleteRequest("/admin/delegate-proxy?specId=01_hello"));
resp.assertHtmlAuthenticationRequired();

// get does nothing for now
resp = apiTestHelper.callWithoutAuth(apiTestHelper.createRequest("/admin/delegate-proxy"));
resp.assertHtmlAuthenticationRequired(); // TODO
}

@Test
public void testNonAdminUser() {
Response resp = apiTestHelper.callWithAuthDemo2(apiTestHelper.createDeleteRequest("/admin/delegate-proxy"));
resp.assertForbidden();

resp = apiTestHelper.callWithAuthDemo2(apiTestHelper.createDeleteRequest("/admin/delegate-proxy?id=" + RANDOM_UUID));
resp.assertForbidden();

resp = apiTestHelper.callWithAuthDemo2(apiTestHelper.createDeleteRequest("/admin/delegate-proxy?specId=01_hello"));
resp.assertForbidden();

// get does nothing for now
resp = apiTestHelper.callWithAuthDemo2(apiTestHelper.createRequest("/admin/delegate-proxy"));
resp.assertForbidden(); // TODO
}

@Test
public void testAdminUser() {
Response resp = apiTestHelper.callWithAuth(apiTestHelper.createDeleteRequest("/admin/delegate-proxy"));
resp.jsonSuccess();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void testWithoutAuth() {
public void testListSpecs() {
Response resp = apiTestHelper.callWithAuth(apiTestHelper.createRequest("/api/proxyspec"));
JsonArray specs = resp.jsonSuccess().asJsonArray();
Assertions.assertEquals(1, specs.size());
Assertions.assertEquals(2, specs.size());
JsonObject spec = specs.getJsonObject(0);
// response may not contain any sensitive values
Assertions.assertEquals(List.of("id", "displayName", "description", "logoWidth", "logoHeight", "logoStyle", "logoClasses"), spec.keySet().stream().toList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ public Request.Builder createPutRequest(String path, String body) {
.url(baseUrl + path);
}

public Request.Builder createDeleteRequest(String path) {
return new Request.Builder()
.delete()
.url(baseUrl + path);
}

public Response callWithAuth(Request.Builder request) {
try {
return new Response(clientDemo.newCall(request.build()).execute());
Expand Down
37 changes: 37 additions & 0 deletions src/test/resources/application-test-delegate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
spring:
session:
store-type: none
data:
redis:
repositories:
enabled: false

proxy:
support:
mail-to-address: test@shinyproxy.io
authentication: simple
container-backend: docker
heartbeat-timeout: -1
default-stop-proxy-on-logout: false

admin-users:
- demo

users:
- name: demo
password: demo
groups:
- group1
- group2
- name: demo2
password: demo2

docker:
url: http://localhost:2375


specs:
- id: 01_hello
display-name: 01-hello
description: my description
minium-seats-available: 1

0 comments on commit 2f75900

Please sign in to comment.