Skip to content

Instantly share code, notes, and snippets.

@Glamdring
Last active January 29, 2019 09:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Glamdring/40a7b16cc90e1306195c0b1ec32e5165 to your computer and use it in GitHub Desktop.
Save Glamdring/40a7b16cc90e1306195c0b1ec32e5165 to your computer and use it in GitHub Desktop.
Controller for integrating a SaaS with Heroku
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