{"action":"created","comment":{"url":"https://api.github.com/repos/rackerlabs/repose/pulls/comments/33688665","id":33688665,"diff_hunk":"@@ -0,0 +1,360 @@\n+/*\n+ * _=_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_=\n+ * Repose\n+ * _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-\n+ * Copyright (C) 2010 - 2015 Rackspace US, Inc.\n+ * _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-\n+ * Licensed under the Apache License, Version 2.0 (the \"License\");\n+ * you may not use this file except in compliance with the License.\n+ * You may obtain a copy of the License at\n+ *\n+ *      http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ * =_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_=_\n+ */\n+package org.openrepose.filters.keystonev2\n+\n+import java.net.URL\n+import javax.inject.{Inject, Named}\n+import javax.servlet._\n+import javax.servlet.http.HttpServletResponse._\n+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}\n+\n+import com.rackspace.httpdelegation.HttpDelegationManager\n+import com.typesafe.scalalogging.slf4j.LazyLogging\n+import org.apache.commons.codec.binary.Base64\n+import org.openrepose.commons.config.manager.UpdateListener\n+import org.openrepose.commons.utils.http._\n+import org.openrepose.commons.utils.servlet.http.MutableHttpServletRequest\n+import org.openrepose.core.filter.FilterConfigHelper\n+import org.openrepose.core.services.config.ConfigurationService\n+import org.openrepose.core.services.datastore.{Datastore, DatastoreService}\n+import org.openrepose.core.services.serviceclient.akka.AkkaServiceClient\n+import org.openrepose.core.systemmodel.SystemModel\n+import org.openrepose.filters.keystonev2.RequestHandler._\n+import org.openrepose.filters.keystonev2.config._\n+\n+import scala.util.{Failure, Success, Try}\n+\n+@Named\n+class KeystoneV2Filter @Inject()(configurationService: ConfigurationService,\n+                                 akkaServiceClient: AkkaServiceClient,\n+                                 datastoreService: DatastoreService)\n+  extends Filter\n+  with HttpDelegationManager\n+  with LazyLogging {\n+\n+  private final val SYSTEM_MODEL_CONFIG = \"system-model.cfg.xml\"\n+  private final val DEFAULT_CONFIG = \"keystone-v2.cfg.xml\"\n+  private final val X_AUTH_PROXY = \"Proxy\"\n+\n+  private var configurationFile: String = DEFAULT_CONFIG\n+  private var sendTraceHeader = true\n+\n+  //Which happens to be the local datastore\n+  private val datastore: Datastore = datastoreService.getDefaultDatastore\n+\n+  var keystoneV2Config: KeystoneV2Config = _\n+\n+  override def init(filterConfig: FilterConfig): Unit = {\n+    configurationFile = new FilterConfigHelper(filterConfig).getFilterConfig(DEFAULT_CONFIG)\n+    logger.info(s\"Initializing Keystone V2 Filter using config $configurationFile\")\n+    val xsdURL: URL = getClass.getResource(\"/META-INF/schema/config/keystone-v2.xsd\")\n+    configurationService.subscribeTo(\n+      SYSTEM_MODEL_CONFIG,\n+      getClass.getResource(\"/META-INF/schema/system-model/system-model.xsd\"),\n+      SystemModelConfigListener,\n+      classOf[SystemModel]\n+    )\n+    configurationService.subscribeTo(\n+      filterConfig.getFilterName,\n+      configurationFile,\n+      xsdURL,\n+      KeystoneV2ConfigListener,\n+      classOf[KeystoneV2Config]\n+    )\n+  }\n+\n+  override def destroy(): Unit = {\n+    configurationService.unsubscribeFrom(configurationFile, KeystoneV2ConfigListener)\n+    configurationService.unsubscribeFrom(SYSTEM_MODEL_CONFIG, SystemModelConfigListener)\n+  }\n+\n+  implicit val autoHeaderToString: HeaderConstant => String = hc => hc.toString\n+\n+  override def doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse, chain: FilterChain): Unit = {\n+    if (!isInitialized) {\n+      logger.error(\"Keystone v2 filter has not yet initialized...\")\n+      servletResponse.asInstanceOf[HttpServletResponse].sendError(SC_INTERNAL_SERVER_ERROR)\n+    } else {\n+      logger.trace(\"Keystone v2 filter processing request...\")\n+\n+      val config = keystoneV2Config\n+      val request = MutableHttpServletRequest.wrap(servletRequest.asInstanceOf[HttpServletRequest])\n+      // Not using the mutable wrapper because it doesn't work properly at the moment, and we don't need to modify the response from further down the chain\n+      val response = servletResponse.asInstanceOf[HttpServletResponse]\n+      val traceId = Option(request.getHeader(CommonHttpHeader.TRACE_GUID.toString)).filter(_ => sendTraceHeader)\n+      val requestHandler = new RequestHandler(config, akkaServiceClient, datastore, traceId)\n+\n+      //Check our whitelist\n+      val whiteListURIs: Option[List[String]] =\n+        for {\n+          jaxbIntermediary <- Option(config.getWhiteList)\n+          regexList <- Option(jaxbIntermediary.getUriRegex)\n+        } yield {\n+          import scala.collection.JavaConversions._\n+          regexList.toList\n+        }\n+\n+      val whiteListMatch: Boolean = whiteListURIs exists { uriList =>\n+        uriList exists { pattern =>\n+          logger.debug(s\"checking ${request.getRequestURI} against $pattern\")\n+          request.getRequestURI.matches(pattern)\n+        }\n+      }\n+\n+      val authTokenValue = Option(request.getHeader(CommonHttpHeader.AUTH_TOKEN))\n+\n+      //Get the authenticating token!\n+      val result: KeystoneV2Result = if (whiteListMatch) {\n+        Pass(Map.empty[String, String])\n+      } else {\n+        authTokenValue map { authToken =>\n+          //This block of code tries to get the token from the datastore, and provides it from real calls, if it isn't\n+          val tokenValidationResult: Try[AuthResult] =\n+            Option(datastore.get(s\"$TOKEN_KEY_PREFIX$authToken\").asInstanceOf[AuthResult]) map { validationResult =>\n+              Success(validationResult)\n+            } getOrElse {\n+              //flatMap to unbox the Try[Try[TokenValidationResult]] so all Failure's are just packaged along\n+              requestHandler.getAdminToken flatMap { adminToken =>\n+                requestHandler.validateToken(adminToken, authToken) recoverWith {\n+                  //Recover if the exception is an AdminTokenUnauthorizedException\n+                  //This way we can specify however we want to what we want to do to retry.\n+                  //Also it only retries ONCE! No loops or anything. Fails gloriously\n+                  case unauth: AdminTokenUnauthorizedException =>\n+                    //Clear the cache, call this method again\n+                    datastore.remove(ADMIN_TOKEN_KEY)\n+                    requestHandler.getAdminToken match {\n+                      case Success(newAdminToken) =>\n+                        requestHandler.validateToken(newAdminToken, authToken)\n+                      case Failure(x) => Failure(IdentityAdminTokenException(\"Unable to reacquire admin token\", x))\n+                    }\n+                }\n+              }\n+            }\n+\n+          tokenValidationResult match {\n+            case Success(InvalidToken) =>\n+              Reject(SC_UNAUTHORIZED)\n+            case Success(validToken: ValidToken) =>\n+              val uriTenantOption = requestHandler.extractTenant(request.getRequestURI)\n+              val authorizedTenant = requestHandler.tenantAuthorization(uriTenantOption, validToken) match {\n+                case Some(Success(tenantHeaderValues)) =>\n+                  Pass(Map(OpenStackServiceHeader.TENANT_ID.toString -> tenantHeaderValues.mkString(\",\")))\n+                case Some(Failure(e)) =>\n+                  Reject(SC_UNAUTHORIZED, failure = Some(e))\n+                case None =>\n+                  Pass(Map.empty[String, String])\n+              }\n+\n+              val endpoints = authorizedTenant match {\n+                case Pass(headers) =>\n+                  requestHandler.handleEndpoints(authToken, validToken) match {\n+                    case Some(Success(endpointsData)) =>\n+                      //If I'm configured to put the endpoints into a x-catalog do it\n+                      if (config.getIdentityService.isSetCatalogInHeader) {\n+                        // todo: don't check this flag twice\n+                        val endpointsHeader = PowerApiHeader.X_CATALOG.toString -> Base64.encodeBase64String(endpointsData.endpointsJson.getBytes)\n+                        Pass(headers + endpointsHeader)\n+                      } else {\n+                        Pass(headers)\n+                      }\n+                    case Some(Failure(x)) =>\n+                      //Reject them with 403\n+                      Reject(SC_FORBIDDEN, failure = Some(x))\n+                    case None =>\n+                      //Do more things in here\n+                      Pass(headers)\n+                  }\n+                case reject: Reject => reject\n+              }\n+\n+              val userGroups = endpoints match {\n+                case Pass(headers) =>\n+                  requestHandler.getGroups(authToken, validToken) match {\n+                    case Some(Success(groups)) =>\n+                      if (groups.isEmpty) {\n+                        Pass(headers)\n+                      } else {\n+                        val groupsHeader = PowerApiHeader.GROUPS.toString -> groups.mkString(\",\")\n+                        Pass(headers + groupsHeader)\n+                      }\n+                    case Some(Failure(e)) =>\n+                      logger.error(s\"Could not get groups: ${e.getMessage}\")\n+                      Pass(headers)\n+                    case None => Pass(headers)\n+                  }\n+                case reject: Reject => reject\n+              }\n+\n+              val addHeaders = userGroups match {\n+                case Pass(headers) =>\n+                  val userHeaders = Map(PowerApiHeader.USER.toString -> validToken.username,\n+                    OpenStackServiceHeader.USER_NAME.toString -> validToken.username,\n+                    OpenStackServiceHeader.USER_ID.toString -> validToken.userId)\n+\n+                  val xAuthHeader = uriTenantOption match {\n+                    case Some(uriTenant) =>\n+                      OpenStackServiceHeader.EXTENDED_AUTHORIZATION.toString -> s\"$X_AUTH_PROXY $uriTenant\"\n+                    case None =>\n+                      OpenStackServiceHeader.EXTENDED_AUTHORIZATION.toString -> X_AUTH_PROXY\n+                  }\n+\n+                  val rolesHeader = if (config.getIdentityService.isSetRolesInHeader) {\n+                    Map(OpenStackServiceHeader.ROLES.toString -> validToken.roles.mkString(\",\"))\n+                  } else {\n+                    Map.empty[String, String]\n+                  }\n+\n+                  val tenantName = OpenStackServiceHeader.TENANT_NAME.toString -> validToken.tenantName\n+\n+                  val defaultRegion = validToken.defaultRegion.map(dr => OpenStackServiceHeader.DEFAULT_REGION.toString -> dr)\n+\n+                  val contactId = validToken.contactId.map(ci => OpenStackServiceHeader.CONTACT_ID.toString -> ci)\n+\n+                  val expirationDate = OpenStackServiceHeader.X_EXPIRATION.toString -> validToken.expirationDate\n+\n+                  val impersonatorId = validToken.impersonatorId.map(iid => OpenStackServiceHeader.IMPERSONATOR_ID.toString -> iid)\n+                  val impersonatorName = validToken.impersonatorName.map(in => OpenStackServiceHeader.IMPERSONATOR_NAME.toString -> in)\n+\n+                  val identityStatus = OpenStackServiceHeader.IDENTITY_STATUS.toString -> IdentityStatus.Confirmed.toString\n+\n+                  Pass(headers ++ userHeaders ++ rolesHeader + tenantName + xAuthHeader ++ defaultRegion ++ contactId\n+                    + expirationDate ++ impersonatorId ++ impersonatorName + identityStatus)\n+                case reject: Reject => reject\n+              }\n+\n+              addHeaders\n+            case Failure(x: IdentityAdminTokenException) =>\n+              Reject(SC_INTERNAL_SERVER_ERROR, failure = Some(x))\n+            case Failure(x: IdentityCommuncationException) =>\n+              Reject(SC_BAD_GATEWAY, failure = Some(x))\n+            case Failure(x) =>\n+              //TODO: this isn't yet complete, should return other status codes?\n+              Reject(SC_INTERNAL_SERVER_ERROR, failure = Some(x))\n+          }\n+        } getOrElse {\n+          //Don't have an auth token to validate\n+          Reject(SC_FORBIDDEN, Some(\"Auth token not found in headers\"))\n+        }\n+      }\n+\n+      //Handle the result of the filter\n+      result match {\n+        case rejection: Reject =>\n+          val message: Option[String] = rejection match {\n+            case Reject(_, Some(x), _) => Some(x)\n+            case Reject(code, None, Some(failure)) => Some(failure.getMessage)\n+            case _ => None\n+          }\n+          //Handle delegation if necessary\n+          Option(config.getDelegating) match {\n+            case Some(delegating) =>\n+              logger.debug(s\"Delegating with status ${rejection.status}\")\n+              val delegationHeaders = buildDelegationHeaders(rejection.status,\n+                \"keystone-v2\",\n+                message.getOrElse(\"Failure in the Keystone v2 filter\").replace(\"\\n\", \"\"),\n+                delegating.getQuality)\n+\n+              delegationHeaders foreach { case (key, values) =>\n+                values foreach { value =>\n+                  request.addHeader(key, value)\n+                }\n+              }\n+              request.addHeader(OpenStackServiceHeader.IDENTITY_STATUS.toString, IdentityStatus.Indeterminate.toString)\n+\n+              chain.doFilter(request, response)\n+            case None =>\n+              logger.debug(s\"Rejecting with status ${rejection.status}\")\n+              response.setHeader(CommonHttpHeader.WWW_AUTHENTICATE.toString, s\"Keystone uri=${config.getIdentityService.getUri}\")\n+              message match {\n+                case Some(m) =>\n+                  logger.debug(s\"Rejection message: $m\")\n+                  response.sendError(rejection.status, m)\n+                case None => response.sendError(rejection.status)","path":"repose-aggregator/components/filters/keystone-v2/src/main/scala/org/openrepose/filters/keystonev2/KeystoneV2Filter.scala","position":289,"original_position":289,"commit_id":"111683a4de8a0228293ef1c4b306ce44869d0413","original_commit_id":"111683a4de8a0228293ef1c4b306ce44869d0413","user":{"login":"dmnjohns","id":1923408,"avatar_url":"https://avatars.githubusercontent.com/u/1923408?v=3","gravatar_id":"","url":"https://api.github.com/users/dmnjohns","html_url":"https://github.com/dmnjohns","followers_url":"https://api.github.com/users/dmnjohns/followers","following_url":"https://api.github.com/users/dmnjohns/following{/other_user}","gists_url":"https://api.github.com/users/dmnjohns/gists{/gist_id}","starred_url":"https://api.github.com/users/dmnjohns/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dmnjohns/subscriptions","organizations_url":"https://api.github.com/users/dmnjohns/orgs","repos_url":"https://api.github.com/users/dmnjohns/repos","events_url":"https://api.github.com/users/dmnjohns/events{/privacy}","received_events_url":"https://api.github.com/users/dmnjohns/received_events","type":"User","site_admin":false},"body":"Should we use sendError here? sendError commits the response stream meaning previous filters in the filter chain cannot modify that portion of the response. I know we use sendError in a couple of places, so I thought I'd call attention to this.\r\n\r\nOf course, if a previous filter uses our new response wrapper, this is a non-issue. It's only the case where a filter is mutating the response directly that we /might/ run into trouble. Now to a standard filter writer, that may be the approach they take since a response is mutable by design.","created_at":"2015-07-01T15:15:52Z","updated_at":"2015-07-01T15:15:52Z","html_url":"https://github.com/rackerlabs/repose/pull/1326#discussion_r33688665","pull_request_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1326","_links":{"self":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/comments/33688665"},"html":{"href":"https://github.com/rackerlabs/repose/pull/1326#discussion_r33688665"},"pull_request":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1326"}}},"pull_request":{"url":"https://api.github.com/repos/rackerlabs/repose/pulls/1326","id":38877626,"html_url":"https://github.com/rackerlabs/repose/pull/1326","diff_url":"https://github.com/rackerlabs/repose/pull/1326.diff","patch_url":"https://github.com/rackerlabs/repose/pull/1326.patch","issue_url":"https://api.github.com/repos/rackerlabs/repose/issues/1326","number":1326,"state":"open","locked":false,"title":"REP-1840 Keystone v2 (Newstone)","user":{"login":"dmnjohns","id":1923408,"avatar_url":"https://avatars.githubusercontent.com/u/1923408?v=3","gravatar_id":"","url":"https://api.github.com/users/dmnjohns","html_url":"https://github.com/dmnjohns","followers_url":"https://api.github.com/users/dmnjohns/followers","following_url":"https://api.github.com/users/dmnjohns/following{/other_user}","gists_url":"https://api.github.com/users/dmnjohns/gists{/gist_id}","starred_url":"https://api.github.com/users/dmnjohns/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dmnjohns/subscriptions","organizations_url":"https://api.github.com/users/dmnjohns/orgs","repos_url":"https://api.github.com/users/dmnjohns/repos","events_url":"https://api.github.com/users/dmnjohns/events{/privacy}","received_events_url":"https://api.github.com/users/dmnjohns/received_events","type":"User","site_admin":false},"body":"So yesterday I got Jenny's basic test to pass. All of the unit tests are currently passing. I expect something will fail when the PR builder does it's thing, but it's time to get some more eyes on this.","created_at":"2015-06-30T13:31:05Z","updated_at":"2015-07-01T15:15:52Z","closed_at":null,"merged_at":null,"merge_commit_sha":"0c61cce14fbbbdcac9a48a2ca69d569c60553e97","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1326/commits","review_comments_url":"https://api.github.com/repos/rackerlabs/repose/pulls/1326/comments","review_comment_url":"https://api.github.com/repos/rackerlabs/repose/pulls/comments{/number}","comments_url":"https://api.github.com/repos/rackerlabs/repose/issues/1326/comments","statuses_url":"https://api.github.com/repos/rackerlabs/repose/statuses/111683a4de8a0228293ef1c4b306ce44869d0413","head":{"label":"rackerlabs:REP-1840-newstonev2","ref":"REP-1840-newstonev2","sha":"111683a4de8a0228293ef1c4b306ce44869d0413","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}","created_at":"2011-07-01T04:39:34Z","updated_at":"2015-07-01T01:41:39Z","pushed_at":"2015-07-01T14:13:32Z","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":118548,"stargazers_count":188,"watchers_count":188,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":54,"mirror_url":null,"open_issues_count":10,"forks":54,"open_issues":10,"watchers":188,"default_branch":"master"}},"base":{"label":"rackerlabs:master","ref":"master","sha":"4ec77102896be51180f0d6f00d2f7b4f5c383c83","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}","created_at":"2011-07-01T04:39:34Z","updated_at":"2015-07-01T01:41:39Z","pushed_at":"2015-07-01T14:13:32Z","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":118548,"stargazers_count":188,"watchers_count":188,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":54,"mirror_url":null,"open_issues_count":10,"forks":54,"open_issues":10,"watchers":188,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1326"},"html":{"href":"https://github.com/rackerlabs/repose/pull/1326"},"issue":{"href":"https://api.github.com/repos/rackerlabs/repose/issues/1326"},"comments":{"href":"https://api.github.com/repos/rackerlabs/repose/issues/1326/comments"},"review_comments":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1326/comments"},"review_comment":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/rackerlabs/repose/pulls/1326/commits"},"statuses":{"href":"https://api.github.com/repos/rackerlabs/repose/statuses/111683a4de8a0228293ef1c4b306ce44869d0413"}}}}