{"action":"created","comment":{"url":"https://api.github.com/repos/rackerlabs/repose/pulls/comments/95677959","pull_request_review_id":16242001,"id":95677959,"diff_hunk":"@@ -20,22 +20,210 @@\n \n package org.openrepose.filters.samlpolicy\n \n+import java.io.InputStream\n+import java.util.concurrent.TimeUnit\n import javax.inject.{Inject, Named}\n-import javax.servlet.{FilterChain, ServletRequest, ServletResponse}\n+import javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR\n+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}\n+import javax.servlet.{FilterChain, ServletInputStream, ServletRequest, ServletResponse}\n+import javax.ws.rs.core.MediaType\n \n+import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache}\n+import com.typesafe.scalalogging.slf4j.LazyLogging\n+import net.sf.saxon.s9api.XsltExecutable\n+import org.openrepose.commons.utils.http.CommonHttpHeader.{CONTENT_LENGTH, CONTENT_TYPE}\n+import org.openrepose.commons.utils.servlet.http.{HttpServletRequestWrapper, HttpServletResponseWrapper, ResponseMode}\n import org.openrepose.core.filter.AbstractConfiguredFilter\n import org.openrepose.core.services.config.ConfigurationService\n+import org.openrepose.core.services.serviceclient.akka.AkkaServiceClientFactory\n import org.openrepose.filters.samlpolicy.config.SamlPolicyConfig\n+import org.openrepose.nodeservice.atomfeed.{AtomFeedListener, AtomFeedService, LifecycleEvents}\n+import org.w3c.dom.Document\n \n /**\n   * Created by adrian on 12/12/16.\n   */\n @Named\n-class SamlPolicyTranslationFilter @Inject()(configurationService: ConfigurationService)\n-  extends AbstractConfiguredFilter[SamlPolicyConfig](configurationService){\n+class SamlPolicyTranslationFilter @Inject()(configurationService: ConfigurationService,\n+                                            atomFeedService: AtomFeedService,\n+                                            akkaServiceClientFactory: AkkaServiceClientFactory)\n+  extends AbstractConfiguredFilter[SamlPolicyConfig](configurationService)\n+  with LazyLogging\n+  with AtomFeedListener {\n \n   override val DEFAULT_CONFIG: String = \"saml-policy.cfg.xml\"\n   override val SCHEMA_LOCATION: String = \"/META-INF/config/schema/saml-policy.xsd\"\n \n-  override def doWork(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = chain.doFilter(request, response)\n+  private var cache: LoadingCache[String, XsltExecutable] = _\n+  private var feedId: Option[String] = None\n+\n+  override def doWork(servletRequest: ServletRequest, servletResponse: ServletResponse, chain: FilterChain): Unit = {\n+    try {\n+      val request = new HttpServletRequestWrapper(servletRequest.asInstanceOf[HttpServletRequest])\n+      var response = servletResponse.asInstanceOf[HttpServletResponse]\n+      val samlResponse = decodeSamlResponse(request)\n+      val rawDocument = readToDom(samlResponse)\n+\n+      val version = determineVersion(rawDocument)\n+      val finalDocument = version match {\n+        case 1 =>\n+          request.addHeader(\"Identity-API-Version\", \"1.0\")\n+          rawDocument\n+        case 2 =>\n+          response = new HttpServletResponseWrapper(response, ResponseMode.PASSTHROUGH, ResponseMode.MUTABLE)\n+          request.addHeader(\"Identity-API-Version\", \"2.0\")\n+          val issuer = validateResponseAndGetIssuer(rawDocument)\n+          val translatedDocument = translateResponse(rawDocument, cache.get(issuer))\n+          signResponse(translatedDocument)\n+      }\n+      val inputStream = convertDocumentToStream(finalDocument)\n+\n+      request.replaceHeader(CONTENT_TYPE.toString, MediaType.APPLICATION_XML)\n+      request.removeHeader(CONTENT_LENGTH.toString)\n+      request.replaceHeader(\"Transfer-Encoding\", \"chunked\")\n+\n+      chain.doFilter(new HttpServletRequestWrapper(request, inputStream), response)\n+\n+      if (version == 2) {\n+        doResponseStuff()\n+        response.asInstanceOf[HttpServletResponseWrapper].commitToResponse()\n+      }\n+    } catch {\n+      case ex: SamlPolicyException =>\n+        servletResponse.asInstanceOf[HttpServletResponse].sendError(ex.statusCode, ex.message)\n+        logger.debug(\"SAML policy translation failed\", ex)\n+      case ex: Exception =>\n+        servletResponse.asInstanceOf[HttpServletResponse].sendError(SC_INTERNAL_SERVER_ERROR, \"Unknown error in SAML Policy Filter\")\n+        logger.warn(\"Unexpected problem in SAML Policy Filter\", ex)\n+    }\n+  }\n+\n+  /**\n+    * Gets the SAMl response from the post encoded body, and decodes it into a plain String.\n+    *\n+    * @param request the servlet request that has an encoded body\n+    * @return the decoded saml response\n+    * @throws SamlPolicyException if decoding fails\n+    */\n+  def decodeSamlResponse(request: HttpServletRequest): InputStream = ???\n+\n+  /**\n+    * Parses a saml response into a dom document.\n+    *\n+    * @param samlResponse the decoded saml response\n+    * @return the corresponding dom object\n+    * @throws SamlPolicyException if parsing fails\n+    */\n+  def readToDom(samlResponse: InputStream): Document = ???\n+\n+  /**\n+    * Reads the parsed saml response and checks the issuer against the filter config\n+    * and determines what version of the saml api we are using.\n+    *\n+    * @param document the parsed saml response\n+    * @return 1 or 2 as appropriate\n+    * @throws SamlPolicyException should it have problems finding the issuer\n+    */\n+  def determineVersion(document: Document): Int = ???\n+\n+  /**\n+    * Determines whether or not the response follows the rules that are required of the saml response.\n+    * Additionally returns the issuer for the contained assertions.\n+    *\n+    * @param document the parsed saml response\n+    * @return the issuer for the embedded assertions\n+    * @throws SamlPolicyException if response is invalid\n+    */\n+  def validateResponseAndGetIssuer(document: Document): String = ???\n+\n+  /**\n+    * Retrieves the policy from the configured endpoint. The caching will be handled elsewhere.\n+    * There is a possiblity that this method will have to get split into two calls is we end up needing the raw policy for the os response mangling.\n+    * I hope not, because that poops on what i'm trying to do with the cache at the moment.\n+    *\n+    * @param issuer the issuer\n+    * @return the compiled xslt that represents the policy\n+    * @throws SamlPolicyException for so many reasons\n+    */\n+  def getPolicy(issuer: String): XsltExecutable = ???\n+\n+  /**\n+    * Applies the policy to the saml response.\n+    *\n+    * @param document the parsed saml response\n+    * @param policy the xslt translation\n+    * @return the translated document\n+    * @throws SamlPolicyException if the translation fails\n+    */\n+  def translateResponse(document: Document, policy: XsltExecutable): Document = ???\n+\n+  /**\n+    * Signs the saml response.\n+    *\n+    * @param document the translated saml response\n+    * @return a signed saml response\n+    * @throws SamlPolicyException if the signing fails\n+    */\n+  def signResponse(document: Document): Document = ???\n+\n+  /**\n+    * Converts the saml response document into a servlet input stream for passing down the filter chain.\n+    *\n+    * @param document the final saml response\n+    * @return the sinput stream that can be wrapped and passed along\n+    */\n+  def convertDocumentToStream(document: Document): ServletInputStream = ???\n+\n+  /**\n+    * This is super vague because we don't know what it looks like yet.\n+    *\n+    * @return\n+    */\n+  def doResponseStuff() = ???\n+\n+  /**\n+    * Evict from the cache as appropriate.\n+    *\n+    * @param atomEntry A {@link String} representation of an Atom entry. Note that Atom entries are XML elements,\n+    */\n+  override def onNewAtomEntry(atomEntry: String): Unit = ???\n+\n+  /**\n+    * I suspect we don't care, but i could be wrong.\n+    *\n+    * @param event A value representing the new lifecycle stage of the system associated with the Feed that this\n+    */\n+  override def onLifecycleEvent(event: LifecycleEvents): Unit = ???\n+\n+  /**\n+    * Stores the configuration and marks the filter as initialized.\n+    * I'm going to have to initialize the cache and atom feed listener here, but it's not in the critical path for the moment.\n+    *\n+    * @param newConfiguration\n+    */\n+  override def configurationUpdated(newConfiguration: SamlPolicyConfig): Unit = {\n+    val requestedFeedId = Option(newConfiguration.getPolicyAcquisition.getCache.getAtomFeedId)\n+    if (feedId.nonEmpty && (requestedFeedId != Option(configuration.getPolicyAcquisition.getCache.getAtomFeedId))) {\n+      atomFeedService.unregisterListener(feedId.get)\n+      feedId = None\n+    }\n+\n+    if (feedId.isEmpty && requestedFeedId.nonEmpty) {\n+      feedId = Option(atomFeedService.registerListener(requestedFeedId.get, this))\n+    }\n+\n+    if (Option(configuration).map(_.getPolicyAcquisition.getCache.getTtl) != Option(newConfiguration.getPolicyAcquisition.getCache.getTtl)) {\n+      cache = CacheBuilder.newBuilder()","path":"repose-aggregator/components/filters/saml-policy-translation-filter/src/main/scala/org/openrepose/filters/samlpolicy/SamlPolicyTranslationFilter.scala","position":201,"original_position":201,"commit_id":"e5ec2918572e0b7bf981e5e23c9bd7eddcbe5e05","original_commit_id":"e5ec2918572e0b7bf981e5e23c9bd7eddcbe5e05","user":{"login":"adrianjgeorge","id":997130,"avatar_url":"https://avatars.githubusercontent.com/u/997130?v=3","gravatar_id":"","url":"https://api.github.com/users/adrianjgeorge","html_url":"https://github.com/adrianjgeorge","followers_url":"https://api.github.com/users/adrianjgeorge/followers","following_url":"https://api.github.com/users/adrianjgeorge/following{/other_user}","gists_url":"https://api.github.com/users/adrianjgeorge/gists{/gist_id}","starred_url":"https://api.github.com/users/adrianjgeorge/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adrianjgeorge/subscriptions","organizations_url":"https://api.github.com/users/adrianjgeorge/orgs","repos_url":"https://api.github.com/users/adrianjgeorge/repos","events_url":"https://api.github.com/users/adrianjgeorge/events{/privacy}","received_events_url":"https://api.github.com/users/adrianjgeorge/received_events","type":"User","site_admin":false},"body":"Anybody got a good idea on how to test this? My best ideas are to expose the cache reflectively to check if the if is working correctly, and do an inheritance of this class to replace getPolicy to test that the ttl and get are working. These are terrible, but as i said my \"best\" ideas.","created_at":"2017-01-11T21:38:46Z","updated_at":"2017-01-11T21:38:46Z","html_url":"https://github.com/rackerlabs/repose/pull/1643#discussion_r95677959","pull_request_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1643","_links":{"self":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/comments/95677959"},"html":{"href":"https://github.com/rackerlabs/repose/pull/1643#discussion_r95677959"},"pull_request":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1643"}}},"pull_request":{"url":"https://api.github.com/repos/rackerlabs/repose/pulls/1643","id":100525896,"html_url":"https://github.com/rackerlabs/repose/pull/1643","diff_url":"https://github.com/rackerlabs/repose/pull/1643.diff","patch_url":"https://github.com/rackerlabs/repose/pull/1643.patch","issue_url":"https://api.github.com/repos/rackerlabs/repose/issues/1643","number":1643,"state":"open","locked":false,"title":"Do NOT Merge: SAML Orchestration","user":{"login":"adrianjgeorge","id":997130,"avatar_url":"https://avatars.githubusercontent.com/u/997130?v=3","gravatar_id":"","url":"https://api.github.com/users/adrianjgeorge","html_url":"https://github.com/adrianjgeorge","followers_url":"https://api.github.com/users/adrianjgeorge/followers","following_url":"https://api.github.com/users/adrianjgeorge/following{/other_user}","gists_url":"https://api.github.com/users/adrianjgeorge/gists{/gist_id}","starred_url":"https://api.github.com/users/adrianjgeorge/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adrianjgeorge/subscriptions","organizations_url":"https://api.github.com/users/adrianjgeorge/orgs","repos_url":"https://api.github.com/users/adrianjgeorge/repos","events_url":"https://api.github.com/users/adrianjgeorge/events{/privacy}","received_events_url":"https://api.github.com/users/adrianjgeorge/received_events","type":"User","site_admin":false},"body":"","created_at":"2017-01-06T21:09:45Z","updated_at":"2017-01-11T21:38:46Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cd21e8f5f74744322b689a28ea3d919696a50975","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1643/commits","review_comments_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1643/comments","review_comment_url":"https://api.github.com/repos/rackerlabs/repose/pulls/comments{/number}","comments_url":"https://api.github.com/repos/rackerlabs/repose/issues/1643/comments","statuses_url":"https://api.github.com/repos/rackerlabs/repose/statuses/e5ec2918572e0b7bf981e5e23c9bd7eddcbe5e05","head":{"label":"rackerlabs:REP-4886","ref":"REP-4886","sha":"e5ec2918572e0b7bf981e5e23c9bd7eddcbe5e05","user":{"login":"rackerlabs","id":84293,"avatar_url":"https://avatars.githubusercontent.com/u/84293?v=3","gravatar_id":"","url":"https://api.github.com/users/rackerlabs","html_url":"https://github.com/rackerlabs","followers_url":"https://api.github.com/users/rackerlabs/followers","following_url":"https://api.github.com/users/rackerlabs/following{/other_user}","gists_url":"https://api.github.com/users/rackerlabs/gists{/gist_id}","starred_url":"https://api.github.com/users/rackerlabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rackerlabs/subscriptions","organizations_url":"https://api.github.com/users/rackerlabs/orgs","repos_url":"https://api.github.com/users/rackerlabs/repos","events_url":"https://api.github.com/users/rackerlabs/events{/privacy}","received_events_url":"https://api.github.com/users/rackerlabs/received_events","type":"Organization","site_admin":false},"repo":{"id":1981821,"name":"repose","full_name":"rackerlabs/repose","owner":{"login":"rackerlabs","id":84293,"avatar_url":"https://avatars.githubusercontent.com/u/84293?v=3","gravatar_id":"","url":"https://api.github.com/users/rackerlabs","html_url":"https://github.com/rackerlabs","followers_url":"https://api.github.com/users/rackerlabs/followers","following_url":"https://api.github.com/users/rackerlabs/following{/other_user}","gists_url":"https://api.github.com/users/rackerlabs/gists{/gist_id}","starred_url":"https://api.github.com/users/rackerlabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rackerlabs/subscriptions","organizations_url":"https://api.github.com/users/rackerlabs/orgs","repos_url":"https://api.github.com/users/rackerlabs/repos","events_url":"https://api.github.com/users/rackerlabs/events{/privacy}","received_events_url":"https://api.github.com/users/rackerlabs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/rackerlabs/repose","description":"The powerful, programmable, API Middleware Platform","fork":false,"url":"https://api.github.com/repos/rackerlabs/repose","forks_url":"https://api.github.com/repos/rackerlabs/repose/forks","keys_url":"https://api.github.com/repos/rackerlabs/repose/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rackerlabs/repose/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rackerlabs/repose/teams","hooks_url":"https://api.github.com/repos/rackerlabs/repose/hooks","issue_events_url":"https://api.github.com/repos/rackerlabs/repose/issues/events{/number}","events_url":"https://api.github.com/repos/rackerlabs/repose/events","assignees_url":"https://api.github.com/repos/rackerlabs/repose/assignees{/user}","branches_url":"https://api.github.com/repos/rackerlabs/repose/branches{/branch}","tags_url":"https://api.github.com/repos/rackerlabs/repose/tags","blobs_url":"https://api.github.com/repos/rackerlabs/repose/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rackerlabs/repose/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rackerlabs/repose/git/refs{/sha}","trees_url":"https://api.github.com/repos/rackerlabs/repose/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rackerlabs/repose/statuses/{sha}","languages_url":"https://api.github.com/repos/rackerlabs/repose/languages","stargazers_url":"https://api.github.com/repos/rackerlabs/repose/stargazers","contributors_url":"https://api.github.com/repos/rackerlabs/repose/contributors","subscribers_url":"https://api.github.com/repos/rackerlabs/repose/subscribers","subscription_url":"https://api.github.com/repos/rackerlabs/repose/subscription","commits_url":"https://api.github.com/repos/rackerlabs/repose/commits{/sha}","git_commits_url":"https://api.github.com/repos/rackerlabs/repose/git/commits{/sha}","comments_url":"https://api.github.com/repos/rackerlabs/repose/comments{/number}","issue_comment_url":"https://api.github.com/repos/rackerlabs/repose/issues/comments{/number}","contents_url":"https://api.github.com/repos/rackerlabs/repose/contents/{+path}","compare_url":"https://api.github.com/repos/rackerlabs/repose/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rackerlabs/repose/merges","archive_url":"https://api.github.com/repos/rackerlabs/repose/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rackerlabs/repose/downloads","issues_url":"https://api.github.com/repos/rackerlabs/repose/issues{/number}","pulls_url":"https://api.github.com/repos/rackerlabs/repose/pulls{/number}","milestones_url":"https://api.github.com/repos/rackerlabs/repose/milestones{/number}","notifications_url":"https://api.github.com/repos/rackerlabs/repose/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rackerlabs/repose/labels{/name}","releases_url":"https://api.github.com/repos/rackerlabs/repose/releases{/id}","deployments_url":"https://api.github.com/repos/rackerlabs/repose/deployments","created_at":"2011-07-01T04:39:34Z","updated_at":"2017-01-11T16:33:28Z","pushed_at":"2017-01-11T21:34:58Z","git_url":"git://github.com/rackerlabs/repose.git","ssh_url":"git@github.com:rackerlabs/repose.git","clone_url":"https://github.com/rackerlabs/repose.git","svn_url":"https://github.com/rackerlabs/repose","homepage":"http://www.openrepose.org/","size":86472,"stargazers_count":269,"watchers_count":269,"language":"Groovy","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":84,"mirror_url":null,"open_issues_count":10,"forks":84,"open_issues":10,"watchers":269,"default_branch":"master"}},"base":{"label":"rackerlabs:master","ref":"master","sha":"006c5e285d8891d3fa8cf7f5e8397d0e1477bab2","user":{"login":"rackerlabs","id":84293,"avatar_url":"https://avatars.githubusercontent.com/u/84293?v=3","gravatar_id":"","url":"https://api.github.com/users/rackerlabs","html_url":"https://github.com/rackerlabs","followers_url":"https://api.github.com/users/rackerlabs/followers","following_url":"https://api.github.com/users/rackerlabs/following{/other_user}","gists_url":"https://api.github.com/users/rackerlabs/gists{/gist_id}","starred_url":"https://api.github.com/users/rackerlabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rackerlabs/subscriptions","organizations_url":"https://api.github.com/users/rackerlabs/orgs","repos_url":"https://api.github.com/users/rackerlabs/repos","events_url":"https://api.github.com/users/rackerlabs/events{/privacy}","received_events_url":"https://api.github.com/users/rackerlabs/received_events","type":"Organization","site_admin":false},"repo":{"id":1981821,"name":"repose","full_name":"rackerlabs/repose","owner":{"login":"rackerlabs","id":84293,"avatar_url":"https://avatars.githubusercontent.com/u/84293?v=3","gravatar_id":"","url":"https://api.github.com/users/rackerlabs","html_url":"https://github.com/rackerlabs","followers_url":"https://api.github.com/users/rackerlabs/followers","following_url":"https://api.github.com/users/rackerlabs/following{/other_user}","gists_url":"https://api.github.com/users/rackerlabs/gists{/gist_id}","starred_url":"https://api.github.com/users/rackerlabs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rackerlabs/subscriptions","organizations_url":"https://api.github.com/users/rackerlabs/orgs","repos_url":"https://api.github.com/users/rackerlabs/repos","events_url":"https://api.github.com/users/rackerlabs/events{/privacy}","received_events_url":"https://api.github.com/users/rackerlabs/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/rackerlabs/repose","description":"The powerful, programmable, API Middleware Platform","fork":false,"url":"https://api.github.com/repos/rackerlabs/repose","forks_url":"https://api.github.com/repos/rackerlabs/repose/forks","keys_url":"https://api.github.com/repos/rackerlabs/repose/keys{/key_id}","collaborators_url":"https://api.github.com/repos/rackerlabs/repose/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/rackerlabs/repose/teams","hooks_url":"https://api.github.com/repos/rackerlabs/repose/hooks","issue_events_url":"https://api.github.com/repos/rackerlabs/repose/issues/events{/number}","events_url":"https://api.github.com/repos/rackerlabs/repose/events","assignees_url":"https://api.github.com/repos/rackerlabs/repose/assignees{/user}","branches_url":"https://api.github.com/repos/rackerlabs/repose/branches{/branch}","tags_url":"https://api.github.com/repos/rackerlabs/repose/tags","blobs_url":"https://api.github.com/repos/rackerlabs/repose/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/rackerlabs/repose/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/rackerlabs/repose/git/refs{/sha}","trees_url":"https://api.github.com/repos/rackerlabs/repose/git/trees{/sha}","statuses_url":"https://api.github.com/repos/rackerlabs/repose/statuses/{sha}","languages_url":"https://api.github.com/repos/rackerlabs/repose/languages","stargazers_url":"https://api.github.com/repos/rackerlabs/repose/stargazers","contributors_url":"https://api.github.com/repos/rackerlabs/repose/contributors","subscribers_url":"https://api.github.com/repos/rackerlabs/repose/subscribers","subscription_url":"https://api.github.com/repos/rackerlabs/repose/subscription","commits_url":"https://api.github.com/repos/rackerlabs/repose/commits{/sha}","git_commits_url":"https://api.github.com/repos/rackerlabs/repose/git/commits{/sha}","comments_url":"https://api.github.com/repos/rackerlabs/repose/comments{/number}","issue_comment_url":"https://api.github.com/repos/rackerlabs/repose/issues/comments{/number}","contents_url":"https://api.github.com/repos/rackerlabs/repose/contents/{+path}","compare_url":"https://api.github.com/repos/rackerlabs/repose/compare/{base}...{head}","merges_url":"https://api.github.com/repos/rackerlabs/repose/merges","archive_url":"https://api.github.com/repos/rackerlabs/repose/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/rackerlabs/repose/downloads","issues_url":"https://api.github.com/repos/rackerlabs/repose/issues{/number}","pulls_url":"https://api.github.com/repos/rackerlabs/repose/pulls{/number}","milestones_url":"https://api.github.com/repos/rackerlabs/repose/milestones{/number}","notifications_url":"https://api.github.com/repos/rackerlabs/repose/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/rackerlabs/repose/labels{/name}","releases_url":"https://api.github.com/repos/rackerlabs/repose/releases{/id}","deployments_url":"https://api.github.com/repos/rackerlabs/repose/deployments","created_at":"2011-07-01T04:39:34Z","updated_at":"2017-01-11T16:33:28Z","pushed_at":"2017-01-11T21:34:58Z","git_url":"git://github.com/rackerlabs/repose.git","ssh_url":"git@github.com:rackerlabs/repose.git","clone_url":"https://github.com/rackerlabs/repose.git","svn_url":"https://github.com/rackerlabs/repose","homepage":"http://www.openrepose.org/","size":86472,"stargazers_count":269,"watchers_count":269,"language":"Groovy","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":84,"mirror_url":null,"open_issues_count":10,"forks":84,"open_issues":10,"watchers":269,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1643"},"html":{"href":"https://github.com/rackerlabs/repose/pull/1643"},"issue":{"href":"https://api.github.com/repos/rackerlabs/repose/issues/1643"},"comments":{"href":"https://api.github.com/repos/rackerlabs/repose/issues/1643/comments"},"review_comments":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1643/comments"},"review_comment":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1643/commits"},"statuses":{"href":"https://api.github.com/repos/rackerlabs/repose/statuses/e5ec2918572e0b7bf981e5e23c9bd7eddcbe5e05"}}}}
{"actor":{"display_login":"adrianjgeorge"}}