Last active
January 29, 2019 09:21
-
-
Save Glamdring/40a7b16cc90e1306195c0b1ec32e5165 to your computer and use it in GitHub Desktop.
Controller for integrating a SaaS with Heroku
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.yourapp.web.external; | |
import com.fasterxml.jackson.annotation.JsonProperty; | |
import com.google.common.collect.Maps; | |
import com.yourapp.dto.UserDetails; | |
import com.yourapp.dto.UserRegistrationRequest; | |
import com.yourapp.entities.Application; | |
import com.yourapp.entities.Organization; | |
import com.yourapp.enums.IntegratedCloudProvider; | |
import com.yourapp.enums.SubscriptionPlanCode; | |
import com.yourapp.service.OrganizationService; | |
import com.yourapp.service.SubscriptionService; | |
import com.yourapp.service.UserService; | |
import com.yourapp.web.security.TokenAuthenticationService; | |
import org.apache.commons.codec.digest.DigestUtils; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.http.*; | |
import org.springframework.stereotype.Controller; | |
import org.springframework.web.bind.annotation.*; | |
import org.springframework.web.client.RestTemplate; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import java.nio.charset.StandardCharsets; | |
import java.util.Base64; | |
import java.util.Map; | |
import java.util.UUID; | |
/** | |
* Controller to handle provisioning accounts as heroku resources | |
*/ | |
@Controller | |
@RequestMapping("/api-external/heroku") | |
public class HerokuController { | |
private static final Logger logger = LoggerFactory.getLogger(HerokuController.class); | |
private static final String SUCCESS_MESSAGE = "Resource has been created and is available!"; | |
private static final String DEFAULT_HEROKU_PLAN = "TEST"; | |
private static final String TOKEN_EXCHANGE_URL = "https://id.heroku.com/oauth/token"; | |
private static final String ENV_VAR_PREFIX = "SENTINEL_TRAILS_"; | |
@Autowired | |
private UserService userService; | |
@Autowired | |
private OrganizationService organizationService; | |
@Autowired | |
private SubscriptionService subscriptionService; | |
@Value("${heroku.oauth.client.secret}") | |
private String oAuthClientSecret; | |
@Value("${heroku.sso.salt}") | |
private String ssoSalt; | |
@Value("${heroku.id}") | |
private String herokuId; | |
@Value("${heroku.password}") | |
private String herokuPassword; | |
private RestTemplate restTemplate = new RestTemplate(); | |
@RequestMapping(value = "/resources", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) | |
@ResponseBody | |
public ProvisionResponse provisionResource(@RequestBody ProvisionRequest provisionRequest, HttpServletRequest httpRequest) { | |
validateBasicAuthentication(httpRequest); | |
logger.info("Received Heroku provisioning request {}", provisionRequest); | |
HttpHeaders headers = new HttpHeaders(); | |
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); | |
headers.add("Accept", "application/vnd.heroku+json; version=3"); | |
// Currently not used. It can be used to obtain actual emails of team members and collaborators | |
// as described here https://devcenter.heroku.com/articles/syncing-user-access-as-an-ecosystem-partner | |
HttpEntity<String> tokenExchangeRequest = new HttpEntity<>("grant_type=authorization_code&code=" | |
+ provisionRequest.getoAuthGrant().getCode() + "&client_secret=" | |
+ oAuthClientSecret, headers); | |
ResponseEntity<TokenExchangeResponse> tokenResponse = restTemplate.exchange(TOKEN_EXCHANGE_URL, | |
HttpMethod.POST, tokenExchangeRequest, TokenExchangeResponse.class); | |
// Dummy email and names. We don't need the email for logging-in, as that is handle through an SSO request | |
String email = provisionRequest.getUuid() + "@heroku.com"; | |
String name = "Heroku user"; | |
String organizationName = "Heroku customer"; | |
UserDetails user = userService.getUserDetailsByCloudProviderId( | |
IntegratedCloudProvider.HEROKU.formId(provisionRequest.getUuid().toString())); | |
if (user == null) { | |
// user not found - create a new one | |
UserRegistrationRequest request = new UserRegistrationRequest(); | |
request.setAttributes(Maps.newHashMap()); | |
// If access and refresh token are obtained, store and encrypt them. | |
//request.getAttributes().put("access_token", encrypt(tokenResponse.getBody().getAccessToken())); | |
//request.getAttributes().put("refresh_token", encrypt(tokenResponse.getBody().getRefreshToken())); | |
request.getAttributes().put("resource_id", provisionRequest.getUuid().toString()); | |
request.setNames(name); | |
request.setEmail(email); | |
request.setOrganizationName(organizationName); | |
request.setSubscriptionPlanCode(provisionRequest.getPlan().toUpperCase()); | |
if (request.getSubscriptionPlanCode().equals(DEFAULT_HEROKU_PLAN)) { | |
request.setSubscriptionPlanCode(SubscriptionPlanCode.FREE.toString()); | |
} | |
logger.info("Registering new user as a result of Heroku resource provisioning"); | |
request.setIntegratedCloudProviderId( | |
IntegratedCloudProvider.HEROKU.formId(provisionRequest.getUuid().toString())); | |
UUID userId = userService.register(request); | |
user = userService.getUserDetailsById(userId); | |
} | |
Organization organization = organizationService.getOrganization(user.getOrganizationId()); | |
ProvisionResponse response = new ProvisionResponse(); | |
response.setId(user.getId()); | |
response.setMessage(SUCCESS_MESSAGE); | |
response.setConfig(Maps.newHashMap()); | |
response.getConfig().put(ENV_VAR_PREFIX + "ORGANIZATION_ID", organization.getId().toString()); | |
response.getConfig().put(ENV_VAR_PREFIX + "ORGANIZATION_SECRET", organization.getSecret()); | |
return response; | |
} | |
@RequestMapping(value = "/resources/{resourceId}", method = RequestMethod.PUT, | |
consumes = MediaType.APPLICATION_JSON_VALUE) | |
@ResponseBody | |
public ProvisionResponse upgradePlan(@PathVariable UUID resourceId, @RequestBody PlanChange planChange, | |
HttpServletRequest request) { | |
validateBasicAuthentication(request); | |
UserDetails user = userService.getUserDetailsByCloudProviderId( | |
IntegratedCloudProvider.HEROKU.formId(resourceId.toString())); | |
Organization org = organizationService.getOrganization(user.getOrganizationId()); | |
org.setSubscriptionPlanId(subscriptionService.getSubscriptionPlanId(planChange.getPlan().toUpperCase())); | |
org.setPaymentsEnabled(true); | |
organizationService.updateOrganization(org, user.getId()); | |
ProvisionResponse response = new ProvisionResponse(); | |
response.setMessage("Successfully changed plan"); | |
return response; | |
} | |
@RequestMapping(value = "/resources/{resourceId}", method = RequestMethod.DELETE, | |
consumes = MediaType.APPLICATION_JSON_VALUE) | |
@ResponseBody | |
public ResponseEntity<?> deprovision(@PathVariable UUID resourceId, HttpServletRequest request) { | |
validateBasicAuthentication(request); | |
logger.info("Deprovisioning {}", resourceId); | |
UserDetails user = userService.getUserDetailsByCloudProviderId( | |
IntegratedCloudProvider.HEROKU.formId(resourceId.toString())); | |
Organization org = organizationService.getOrganization(user.getOrganizationId()); | |
org.setSubscriptionPlanId(subscriptionService.getSubscriptionPlanId(SubscriptionPlanCode.FREE.toString())); | |
org.setPaymentsEnabled(false); | |
org.setName("Deprovisioned Heroku customer"); | |
organizationService.updateOrganization(org, user.getId()); | |
return new ResponseEntity<Void>(HttpStatus.NO_CONTENT); | |
} | |
@RequestMapping(value = "/sso", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) | |
public String signin(HttpServletRequest request, HttpServletResponse response) { | |
String resourceId = request.getParameter("resource_id"); | |
String resourceToken = request.getParameter("resource_token"); | |
String timestamp = request.getParameter("timestamp"); | |
String expectedToken = DigestUtils.sha1Hex(resourceId + ":" + ssoSalt + ":" + timestamp).toLowerCase(); | |
if (expectedToken.equals(resourceToken)) { | |
UserDetails user = userService.getUserDetailsByCloudProviderId( | |
IntegratedCloudProvider.HEROKU.formId(resourceId)); | |
TokenAuthenticationService.addAuthentication(response, user.getId()); | |
return "redirect:/"; | |
} else { | |
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | |
return "/"; | |
} | |
} | |
private void validateBasicAuthentication(HttpServletRequest request) { | |
String authorizationHeader = request.getHeader("Authorization"); | |
String credentials = new String(Base64.getDecoder() | |
.decode(authorizationHeader.replace("Basic ", "")), StandardCharsets.UTF_8); | |
// credentials = username:password | |
final String[] values = credentials.split(":", 2); | |
if (!values[0].equals(herokuId) || !values[1].equals(herokuPassword)) { | |
throw new IllegalArgumentException("Credentials invalid"); | |
} | |
} | |
/** | |
* Container for provision requests | |
*/ | |
public static class ProvisionRequest { | |
@JsonProperty("callback_url") | |
private String callbackUrl; | |
private String name; | |
@JsonProperty("oauth_grant") | |
private OAuthGrant oAuthGrant; | |
private Map<String, String> options; | |
private String plan; | |
private String region; | |
private UUID uuid; | |
public String getCallbackUrl() { | |
return callbackUrl; | |
} | |
public void setCallbackUrl(String callbackUrl) { | |
this.callbackUrl = callbackUrl; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public OAuthGrant getoAuthGrant() { | |
return oAuthGrant; | |
} | |
public void setoAuthGrant(OAuthGrant oAuthGrant) { | |
this.oAuthGrant = oAuthGrant; | |
} | |
public Map<String, String> getOptions() { | |
return options; | |
} | |
public void setOptions(Map<String, String> options) { | |
this.options = options; | |
} | |
public String getPlan() { | |
return plan; | |
} | |
public void setPlan(String plan) { | |
this.plan = plan; | |
} | |
public String getRegion() { | |
return region; | |
} | |
public void setRegion(String region) { | |
this.region = region; | |
} | |
public UUID getUuid() { | |
return uuid; | |
} | |
public void setUuid(UUID uuid) { | |
this.uuid = uuid; | |
} | |
} | |
/** | |
* Container for OAuth grants | |
*/ | |
public static class OAuthGrant { | |
@JsonProperty("expires_at") | |
private String expiresAt; | |
private String type; | |
private String code; | |
public String getExpiresAt() { | |
return expiresAt; | |
} | |
public void setExpiresAt(String expiresAt) { | |
this.expiresAt = expiresAt; | |
} | |
public String getType() { | |
return type; | |
} | |
public void setType(String type) { | |
this.type = type; | |
} | |
public String getCode() { | |
return code; | |
} | |
public void setCode(String code) { | |
this.code = code; | |
} | |
} | |
/** | |
* Container for provision responses | |
*/ | |
public static class ProvisionResponse { | |
private UUID id; | |
private String message; | |
private Map<String, String> config; | |
public UUID getId() { | |
return id; | |
} | |
public void setId(UUID id) { | |
this.id = id; | |
} | |
public String getMessage() { | |
return message; | |
} | |
public void setMessage(String message) { | |
this.message = message; | |
} | |
public Map<String, String> getConfig() { | |
return config; | |
} | |
public void setConfig(Map<String, String> config) { | |
this.config = config; | |
} | |
} | |
/** | |
* Container for token exchange responses | |
*/ | |
public static class TokenExchangeResponse { | |
@JsonProperty("access_token") | |
private String accessToken; | |
@JsonProperty("refresh_token") | |
private String refreshToken; | |
@JsonProperty("expires_at") | |
private String expiresAt; | |
@JsonProperty("token_Type") | |
private String tokenType; | |
public String getAccessToken() { | |
return accessToken; | |
} | |
public void setAccessToken(String accessToken) { | |
this.accessToken = accessToken; | |
} | |
public String getRefreshToken() { | |
return refreshToken; | |
} | |
public void setRefreshToken(String refreshToken) { | |
this.refreshToken = refreshToken; | |
} | |
public String getExpiresAt() { | |
return expiresAt; | |
} | |
public void setExpiresAt(String expiresAt) { | |
this.expiresAt = expiresAt; | |
} | |
public String getTokenType() { | |
return tokenType; | |
} | |
public void setTokenType(String tokenType) { | |
this.tokenType = tokenType; | |
} | |
} | |
/** | |
* Container for plan change requests | |
*/ | |
public static class PlanChange { | |
private String plan; | |
public String getPlan() { | |
return plan; | |
} | |
public void setPlan(String plan) { | |
this.plan = plan; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment