{"action":"created","comment":{"url":"https://api.github.com/repos/spotify/styx/pulls/comments/240582342","pull_request_review_id":183641292,"id":240582342,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0MDU4MjM0Mg==","diff_hunk":"@@ -0,0 +1,445 @@\n+/*-\n+ * -\\-\\-\n+ * Spotify Styx API Service\n+ * --\n+ * Copyright (C) 2016 - 2018 Spotify AB\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+\n+package com.spotify.styx.api;\n+\n+import static com.github.rholder.retry.WaitStrategies.exponentialWait;\n+import static com.github.rholder.retry.WaitStrategies.randomWait;\n+import static com.spotify.apollo.Status.BAD_REQUEST;\n+import static com.spotify.apollo.Status.FORBIDDEN;\n+import static java.lang.Boolean.TRUE;\n+import static java.util.concurrent.TimeUnit.SECONDS;\n+import static java.util.stream.Collectors.toList;\n+\n+import com.github.rholder.retry.Attempt;\n+import com.github.rholder.retry.RetryException;\n+import com.github.rholder.retry.Retryer;\n+import com.github.rholder.retry.RetryerBuilder;\n+import com.github.rholder.retry.StopStrategies;\n+import com.github.rholder.retry.StopStrategy;\n+import com.github.rholder.retry.WaitStrategies;\n+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;\n+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;\n+import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;\n+import com.google.api.client.googleapis.json.GoogleJsonResponseException;\n+import com.google.api.client.googleapis.util.Utils;\n+import com.google.api.client.http.HttpTransport;\n+import com.google.api.client.json.JsonFactory;\n+import com.google.api.services.admin.directory.Directory;\n+import com.google.api.services.cloudresourcemanager.CloudResourceManager;\n+import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest;\n+import com.google.api.services.iam.v1.Iam;\n+import com.google.common.cache.Cache;\n+import com.google.common.cache.CacheBuilder;\n+import com.google.common.collect.ImmutableSet;\n+import com.spotify.apollo.Response;\n+import com.spotify.styx.model.WorkflowId;\n+import java.io.IOException;\n+import java.security.GeneralSecurityException;\n+import java.util.List;\n+import java.util.Objects;\n+import java.util.Optional;\n+import java.util.Set;\n+import java.util.concurrent.Callable;\n+import java.util.concurrent.ExecutionException;\n+import java.util.concurrent.atomic.AtomicBoolean;\n+import java.util.function.Supplier;\n+import java.util.regex.Matcher;\n+import java.util.regex.Pattern;\n+import java.util.stream.Stream;\n+import javaslang.Tuple;\n+import javaslang.Tuple2;\n+import javaslang.control.Either;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+/**\n+ * Verifies that a user is authorized to use a service account in a workflow by verifying that the user has\n+ * a specific role either on the service account itself or in the project of the service account.\n+ * <p> This requires the following permissions:<ul>\n+ * <li> {@code resourcemanager.projects.getIamPolicy}\n+ * <li> {@code iam.serviceAccounts.getIamPolicy}\n+ * </ul></p>\n+ */\n+@FunctionalInterface\n+public interface ServiceAccountUsageAuthorizer {\n+\n+  void authorizeServiceAccountUsage(WorkflowId workflowId, String serviceAccount,\n+      GoogleIdToken idToken);\n+\n+  class Impl implements ServiceAccountUsageAuthorizer {\n+\n+    private static final Logger log = LoggerFactory.getLogger(ServiceAccountUsageAuthorizer.class);\n+\n+    private static final Pattern SERVICE_ACCOUNT_PATTERN =\n+        Pattern.compile(\"^.+\\\\.gserviceaccount\\\\.com$\");\n+\n+    private static final Pattern USER_CREATED_SERVICE_ACCOUNT_PATTERN =\n+        Pattern.compile(\"^.+@(.+)\\\\.iam\\\\.gserviceaccount\\\\.com$\");\n+\n+    private static final StopStrategy DEFAULT_RETRY_STOP_STRATEGY = StopStrategies.stopAfterDelay(10, SECONDS);\n+\n+    private static final String CACHE_HIT = \"hit\";\n+    private static final String CACHE_MISS = \"miss\";\n+\n+    private final Iam iam;\n+    private final CloudResourceManager crm;\n+    private final Directory directory;\n+    private final String serviceAccountUserRole;\n+    private final AuthorizationPolicy authorizationPolicy;\n+    private final StopStrategy retryStopStrategy;\n+\n+    /**\n+     * (principalEmail, serviceAccount) -> Either[ResponseException, [Optional[accessMessage]]\n+     */\n+    private final Cache<Tuple2<String, String>, Either<Response<?>, Optional<String>>> cache =\n+        CacheBuilder.newBuilder()\n+            .expireAfterWrite(60, SECONDS)\n+            .maximumSize(10_000)\n+            .build();\n+\n+    Impl(Iam iam, CloudResourceManager crm, Directory directory, String serviceAccountUserRole,\n+        AuthorizationPolicy authorizationPolicy, StopStrategy retryStopStrategy) {\n+      this.iam = Objects.requireNonNull(iam, \"iam\");\n+      this.crm = Objects.requireNonNull(crm, \"crm\");\n+      this.directory = Objects.requireNonNull(directory, \"directory\");\n+      this.serviceAccountUserRole = Objects.requireNonNull(serviceAccountUserRole, \"serviceAccountUserRole\");\n+      this.authorizationPolicy = Objects.requireNonNull(authorizationPolicy, \"authorizationPolicy\");\n+      this.retryStopStrategy = Objects.requireNonNull(retryStopStrategy, \"retryStopStrategy\");\n+    }\n+\n+    @Override\n+    public void authorizeServiceAccountUsage(WorkflowId workflowId, String serviceAccount, GoogleIdToken idToken) {\n+\n+      final boolean enforce = authorizationPolicy.shouldEnforceAuthorization(workflowId, serviceAccount, idToken);\n+\n+      final String principalEmail = idToken.getPayload().getEmail();\n+      final String projectId = serviceAccountProjectId(serviceAccount);\n+\n+      final AtomicBoolean cached = new AtomicBoolean(true);\n+\n+      // Cached access check\n+      final Either<Response<?>, Optional<String>> result = get(cache, Tuple.of(principalEmail, serviceAccount), () -> {\n+        cached.set(false);\n+\n+        try {\n+          return Either.right(firstPresent(\n+              // Check if the principal has been granted the service account user role in the project of the SA\n+              () -> projectPolicyAccess(projectId, principalEmail)\n+                  .map(type -> String.format(\"Principal %s has role %s on service account %s %s\",\n+                      principalEmail, serviceAccountUserRole, serviceAccount, type)),\n+\n+              // Check if the principal has been granted the service account user role on the SA itself\n+              () -> serviceAccountPolicyAccess(serviceAccount, principalEmail)\n+                  .map(type -> String.format(\"Principal %s has role %s on service account %s %s\",\n+                      principalEmail, serviceAccountUserRole, serviceAccount, type))));\n+        } catch (ResponseException e) {\n+          return Either.left(e.getResponse());\n+        }\n+      });\n+\n+      // Propagate response exception\n+      if (result.isLeft()) {\n+        throw new ResponseException(result.left().get());\n+      }\n+\n+      final Optional<String> accessMessage = result.right().get();\n+\n+      // Grant access?\n+      if (accessMessage.isPresent()) {\n+        logAuthorization(workflowId, serviceAccount, enforce, accessMessage.get(), cached.get());\n+        return;\n+      }\n+\n+      // Deny access?\n+      logDenial(workflowId, serviceAccount, enforce, principalEmail, cached.get());\n+      if (enforce) {\n+        throw denialResponseException(serviceAccount, principalEmail, projectId);\n+      }\n+    }\n+\n+    private static <K, V> V get(Cache<K, V> cache, K key, Callable<V> loader) {\n+      try {\n+        return cache.get(key, loader);\n+      } catch (ExecutionException e) {\n+        throw new RuntimeException(e);\n+      }\n+    }\n+\n+    private ResponseException denialResponseException(String serviceAccount, String principalEmail, String projectId) {\n+      return new ResponseException(Response.forStatus(\n+          FORBIDDEN.withReasonPhrase(\"The user \" + principalEmail + \" must have the role \" + serviceAccountUserRole\n+              + \" on the project \" + projectId + \" or the service account \" + serviceAccount +\n+              \", either through a group membership (recommended) or directly\")));\n+    }\n+\n+    private void logDenial(WorkflowId workflowId, String serviceAccount, boolean enforce, String principalEmail,\n+        boolean cached) {\n+      log.info(\"[AUDIT] Principal {} denied use of service account {} in workflow {} (Enforcing: {}, Cache: {})\",\n+          principalEmail, serviceAccount, workflowId.toKey(), enforce, cached ? CACHE_HIT : CACHE_MISS);\n+    }\n+\n+    private void logAuthorization(WorkflowId workflowId, String serviceAccount, boolean enforce, String accessMessage,\n+        boolean cached) {\n+      log.info(\"[AUDIT] {}, authorizing use of service account {} in workflow {} (Enforcing: {}, Cache: {})\",\n+          accessMessage, serviceAccount, workflowId.toKey(), enforce, cached ? CACHE_HIT : CACHE_MISS);\n+    }\n+\n+    private String serviceAccountProjectId(String serviceAccount) {\n+      final Matcher matcher = USER_CREATED_SERVICE_ACCOUNT_PATTERN.matcher(serviceAccount);\n+      if (!matcher.matches()) {\n+        throw new ResponseException(Response.forStatus(\n+            BAD_REQUEST.withReasonPhrase(\"Not a user created service account: \" + serviceAccount)));","path":"styx-api-service/src/main/java/com/spotify/styx/api/ServiceAccountUsageAuthorizer.java","position":209,"original_position":209,"commit_id":"2faa00e3476fc0e1f17c440f19fae8e839552c66","original_commit_id":"2faa00e3476fc0e1f17c440f19fae8e839552c66","user":{"login":"honnix","id":158892,"node_id":"MDQ6VXNlcjE1ODg5Mg==","avatar_url":"https://avatars0.githubusercontent.com/u/158892?v=4","gravatar_id":"","url":"https://api.github.com/users/honnix","html_url":"https://github.com/honnix","followers_url":"https://api.github.com/users/honnix/followers","following_url":"https://api.github.com/users/honnix/following{/other_user}","gists_url":"https://api.github.com/users/honnix/gists{/gist_id}","starred_url":"https://api.github.com/users/honnix/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/honnix/subscriptions","organizations_url":"https://api.github.com/users/honnix/orgs","repos_url":"https://api.github.com/users/honnix/repos","events_url":"https://api.github.com/users/honnix/events{/privacy}","received_events_url":"https://api.github.com/users/honnix/received_events","type":"User","site_admin":false},"body":"`Authenticator` allows for example GCE default service account. Shall we allow that as well here?","created_at":"2018-12-11T12:03:26Z","updated_at":"2018-12-11T12:03:26Z","html_url":"https://github.com/spotify/styx/pull/630#discussion_r240582342","pull_request_url":"https://api.github.com/repos/spotify/styx/pulls/630","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/spotify/styx/pulls/comments/240582342"},"html":{"href":"https://github.com/spotify/styx/pull/630#discussion_r240582342"},"pull_request":{"href":"https://api.github.com/repos/spotify/styx/pulls/630"}}},"pull_request":{"url":"https://api.github.com/repos/spotify/styx/pulls/630","id":232879580,"node_id":"MDExOlB1bGxSZXF1ZXN0MjMyODc5NTgw","html_url":"https://github.com/spotify/styx/pull/630","diff_url":"https://github.com/spotify/styx/pull/630.diff","patch_url":"https://github.com/spotify/styx/pull/630.patch","issue_url":"https://api.github.com/repos/spotify/styx/issues/630","number":630,"state":"open","locked":false,"title":"authorize workflow service account use","user":{"login":"danielnorberg","id":393289,"node_id":"MDQ6VXNlcjM5MzI4OQ==","avatar_url":"https://avatars1.githubusercontent.com/u/393289?v=4","gravatar_id":"","url":"https://api.github.com/users/danielnorberg","html_url":"https://github.com/danielnorberg","followers_url":"https://api.github.com/users/danielnorberg/followers","following_url":"https://api.github.com/users/danielnorberg/following{/other_user}","gists_url":"https://api.github.com/users/danielnorberg/gists{/gist_id}","starred_url":"https://api.github.com/users/danielnorberg/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/danielnorberg/subscriptions","organizations_url":"https://api.github.com/users/danielnorberg/orgs","repos_url":"https://api.github.com/users/danielnorberg/repos","events_url":"https://api.github.com/users/danielnorberg/events{/privacy}","received_events_url":"https://api.github.com/users/danielnorberg/received_events","type":"User","site_admin":false},"body":"# Hey, I just made a Pull Request!\r\n\r\n## Description\r\nAuthorize workflow service account use\r\n\r\n## Motivation and Context\r\nRestrict users to only use _their_ service accounts.\r\n\r\n## Have you tested this? If so, how?\r\n<!--- Valid responses are \"I have included unit tests.\" or --> \r\n<!--- \"I ran my jobs with this code and it works for me.\" -->\r\n\r\n## Checklist for PR author(s)\r\n<!--- Put an `x` in all the boxes that apply: -->\r\n- [x] Changes are covered by unit test\r\n- [x] All tests pass\r\n- [x] Code coverage check passes\r\n- [ ] Error handling is tested\r\n- [ ] Errors are handled at the appropriate layer\r\n- [ ] Errors that cannot be handled where they occur are propagated\r\n- [ ] (optional) Changes are covered by system test\r\n- [ ] Relevant documentation updated\r\n- [ ] This PR has NO breaking change to public API\r\n- [ ] This PR has breaking change to public API and it is documented\r\n\r\n## Checklist for PR reviewer(s)\r\n<!--- Put an `x` in all the boxes that apply: -->\r\n- [ ] This PR has been incorporated in release note for the coming version\r\n- [ ] Risky changes introduced by this PR have been all considered\r\n\r\n<!---\r\nfor more information on how to submit valuable contributions,\r\nsee https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution\r\n-->\r\n","created_at":"2018-11-22T07:15:39Z","updated_at":"2018-12-11T12:03:26Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2e86a4aa1da8223c3060b12fdb6664fc0fae9600","assignee":null,"assignees":[],"requested_reviewers":[{"login":"NatashaL","id":4315174,"node_id":"MDQ6VXNlcjQzMTUxNzQ=","avatar_url":"https://avatars1.githubusercontent.com/u/4315174?v=4","gravatar_id":"","url":"https://api.github.com/users/NatashaL","html_url":"https://github.com/NatashaL","followers_url":"https://api.github.com/users/NatashaL/followers","following_url":"https://api.github.com/users/NatashaL/following{/other_user}","gists_url":"https://api.github.com/users/NatashaL/gists{/gist_id}","starred_url":"https://api.github.com/users/NatashaL/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/NatashaL/subscriptions","organizations_url":"https://api.github.com/users/NatashaL/orgs","repos_url":"https://api.github.com/users/NatashaL/repos","events_url":"https://api.github.com/users/NatashaL/events{/privacy}","received_events_url":"https://api.github.com/users/NatashaL/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/spotify/styx/pulls/630/commits","review_comments_url":"https://api.github.com/repos/spotify/styx/pulls/630/comments","review_comment_url":"https://api.github.com/repos/spotify/styx/pulls/comments{/number}","comments_url":"https://api.github.com/repos/spotify/styx/issues/630/comments","statuses_url":"https://api.github.com/repos/spotify/styx/statuses/2faa00e3476fc0e1f17c440f19fae8e839552c66","head":{"label":"spotify:authorize-service-account-use","ref":"authorize-service-account-use","sha":"2faa00e3476fc0e1f17c440f19fae8e839552c66","user":{"login":"spotify","id":251374,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1MTM3NA==","avatar_url":"https://avatars2.githubusercontent.com/u/251374?v=4","gravatar_id":"","url":"https://api.github.com/users/spotify","html_url":"https://github.com/spotify","followers_url":"https://api.github.com/users/spotify/followers","following_url":"https://api.github.com/users/spotify/following{/other_user}","gists_url":"https://api.github.com/users/spotify/gists{/gist_id}","starred_url":"https://api.github.com/users/spotify/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spotify/subscriptions","organizations_url":"https://api.github.com/users/spotify/orgs","repos_url":"https://api.github.com/users/spotify/repos","events_url":"https://api.github.com/users/spotify/events{/privacy}","received_events_url":"https://api.github.com/users/spotify/received_events","type":"Organization","site_admin":false},"repo":{"id":74431644,"node_id":"MDEwOlJlcG9zaXRvcnk3NDQzMTY0NA==","name":"styx","full_name":"spotify/styx","private":false,"owner":{"login":"spotify","id":251374,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1MTM3NA==","avatar_url":"https://avatars2.githubusercontent.com/u/251374?v=4","gravatar_id":"","url":"https://api.github.com/users/spotify","html_url":"https://github.com/spotify","followers_url":"https://api.github.com/users/spotify/followers","following_url":"https://api.github.com/users/spotify/following{/other_user}","gists_url":"https://api.github.com/users/spotify/gists{/gist_id}","starred_url":"https://api.github.com/users/spotify/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spotify/subscriptions","organizations_url":"https://api.github.com/users/spotify/orgs","repos_url":"https://api.github.com/users/spotify/repos","events_url":"https://api.github.com/users/spotify/events{/privacy}","received_events_url":"https://api.github.com/users/spotify/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spotify/styx","description":"\"The path to execution\", Styx is a service that schedules batch data processing jobs in Docker containers on Kubernetes.","fork":false,"url":"https://api.github.com/repos/spotify/styx","forks_url":"https://api.github.com/repos/spotify/styx/forks","keys_url":"https://api.github.com/repos/spotify/styx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spotify/styx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spotify/styx/teams","hooks_url":"https://api.github.com/repos/spotify/styx/hooks","issue_events_url":"https://api.github.com/repos/spotify/styx/issues/events{/number}","events_url":"https://api.github.com/repos/spotify/styx/events","assignees_url":"https://api.github.com/repos/spotify/styx/assignees{/user}","branches_url":"https://api.github.com/repos/spotify/styx/branches{/branch}","tags_url":"https://api.github.com/repos/spotify/styx/tags","blobs_url":"https://api.github.com/repos/spotify/styx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spotify/styx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spotify/styx/git/refs{/sha}","trees_url":"https://api.github.com/repos/spotify/styx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spotify/styx/statuses/{sha}","languages_url":"https://api.github.com/repos/spotify/styx/languages","stargazers_url":"https://api.github.com/repos/spotify/styx/stargazers","contributors_url":"https://api.github.com/repos/spotify/styx/contributors","subscribers_url":"https://api.github.com/repos/spotify/styx/subscribers","subscription_url":"https://api.github.com/repos/spotify/styx/subscription","commits_url":"https://api.github.com/repos/spotify/styx/commits{/sha}","git_commits_url":"https://api.github.com/repos/spotify/styx/git/commits{/sha}","comments_url":"https://api.github.com/repos/spotify/styx/comments{/number}","issue_comment_url":"https://api.github.com/repos/spotify/styx/issues/comments{/number}","contents_url":"https://api.github.com/repos/spotify/styx/contents/{+path}","compare_url":"https://api.github.com/repos/spotify/styx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spotify/styx/merges","archive_url":"https://api.github.com/repos/spotify/styx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spotify/styx/downloads","issues_url":"https://api.github.com/repos/spotify/styx/issues{/number}","pulls_url":"https://api.github.com/repos/spotify/styx/pulls{/number}","milestones_url":"https://api.github.com/repos/spotify/styx/milestones{/number}","notifications_url":"https://api.github.com/repos/spotify/styx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spotify/styx/labels{/name}","releases_url":"https://api.github.com/repos/spotify/styx/releases{/id}","deployments_url":"https://api.github.com/repos/spotify/styx/deployments","created_at":"2016-11-22T03:44:55Z","updated_at":"2018-12-07T08:25:05Z","pushed_at":"2018-12-11T06:35:02Z","git_url":"git://github.com/spotify/styx.git","ssh_url":"git@github.com:spotify/styx.git","clone_url":"https://github.com/spotify/styx.git","svn_url":"https://github.com/spotify/styx","homepage":"","size":6366,"stargazers_count":122,"watchers_count":122,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":22,"mirror_url":null,"archived":false,"open_issues_count":18,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":22,"open_issues":18,"watchers":122,"default_branch":"master"}},"base":{"label":"spotify:master","ref":"master","sha":"11cef3d77f6a6554519918eb4cd1de6612864bfb","user":{"login":"spotify","id":251374,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1MTM3NA==","avatar_url":"https://avatars2.githubusercontent.com/u/251374?v=4","gravatar_id":"","url":"https://api.github.com/users/spotify","html_url":"https://github.com/spotify","followers_url":"https://api.github.com/users/spotify/followers","following_url":"https://api.github.com/users/spotify/following{/other_user}","gists_url":"https://api.github.com/users/spotify/gists{/gist_id}","starred_url":"https://api.github.com/users/spotify/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spotify/subscriptions","organizations_url":"https://api.github.com/users/spotify/orgs","repos_url":"https://api.github.com/users/spotify/repos","events_url":"https://api.github.com/users/spotify/events{/privacy}","received_events_url":"https://api.github.com/users/spotify/received_events","type":"Organization","site_admin":false},"repo":{"id":74431644,"node_id":"MDEwOlJlcG9zaXRvcnk3NDQzMTY0NA==","name":"styx","full_name":"spotify/styx","private":false,"owner":{"login":"spotify","id":251374,"node_id":"MDEyOk9yZ2FuaXphdGlvbjI1MTM3NA==","avatar_url":"https://avatars2.githubusercontent.com/u/251374?v=4","gravatar_id":"","url":"https://api.github.com/users/spotify","html_url":"https://github.com/spotify","followers_url":"https://api.github.com/users/spotify/followers","following_url":"https://api.github.com/users/spotify/following{/other_user}","gists_url":"https://api.github.com/users/spotify/gists{/gist_id}","starred_url":"https://api.github.com/users/spotify/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/spotify/subscriptions","organizations_url":"https://api.github.com/users/spotify/orgs","repos_url":"https://api.github.com/users/spotify/repos","events_url":"https://api.github.com/users/spotify/events{/privacy}","received_events_url":"https://api.github.com/users/spotify/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/spotify/styx","description":"\"The path to execution\", Styx is a service that schedules batch data processing jobs in Docker containers on Kubernetes.","fork":false,"url":"https://api.github.com/repos/spotify/styx","forks_url":"https://api.github.com/repos/spotify/styx/forks","keys_url":"https://api.github.com/repos/spotify/styx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/spotify/styx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/spotify/styx/teams","hooks_url":"https://api.github.com/repos/spotify/styx/hooks","issue_events_url":"https://api.github.com/repos/spotify/styx/issues/events{/number}","events_url":"https://api.github.com/repos/spotify/styx/events","assignees_url":"https://api.github.com/repos/spotify/styx/assignees{/user}","branches_url":"https://api.github.com/repos/spotify/styx/branches{/branch}","tags_url":"https://api.github.com/repos/spotify/styx/tags","blobs_url":"https://api.github.com/repos/spotify/styx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/spotify/styx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/spotify/styx/git/refs{/sha}","trees_url":"https://api.github.com/repos/spotify/styx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/spotify/styx/statuses/{sha}","languages_url":"https://api.github.com/repos/spotify/styx/languages","stargazers_url":"https://api.github.com/repos/spotify/styx/stargazers","contributors_url":"https://api.github.com/repos/spotify/styx/contributors","subscribers_url":"https://api.github.com/repos/spotify/styx/subscribers","subscription_url":"https://api.github.com/repos/spotify/styx/subscription","commits_url":"https://api.github.com/repos/spotify/styx/commits{/sha}","git_commits_url":"https://api.github.com/repos/spotify/styx/git/commits{/sha}","comments_url":"https://api.github.com/repos/spotify/styx/comments{/number}","issue_comment_url":"https://api.github.com/repos/spotify/styx/issues/comments{/number}","contents_url":"https://api.github.com/repos/spotify/styx/contents/{+path}","compare_url":"https://api.github.com/repos/spotify/styx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/spotify/styx/merges","archive_url":"https://api.github.com/repos/spotify/styx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/spotify/styx/downloads","issues_url":"https://api.github.com/repos/spotify/styx/issues{/number}","pulls_url":"https://api.github.com/repos/spotify/styx/pulls{/number}","milestones_url":"https://api.github.com/repos/spotify/styx/milestones{/number}","notifications_url":"https://api.github.com/repos/spotify/styx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/spotify/styx/labels{/name}","releases_url":"https://api.github.com/repos/spotify/styx/releases{/id}","deployments_url":"https://api.github.com/repos/spotify/styx/deployments","created_at":"2016-11-22T03:44:55Z","updated_at":"2018-12-07T08:25:05Z","pushed_at":"2018-12-11T06:35:02Z","git_url":"git://github.com/spotify/styx.git","ssh_url":"git@github.com:spotify/styx.git","clone_url":"https://github.com/spotify/styx.git","svn_url":"https://github.com/spotify/styx","homepage":"","size":6366,"stargazers_count":122,"watchers_count":122,"language":"Java","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":22,"mirror_url":null,"archived":false,"open_issues_count":18,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0","node_id":"MDc6TGljZW5zZTI="},"forks":22,"open_issues":18,"watchers":122,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/spotify/styx/pulls/630"},"html":{"href":"https://github.com/spotify/styx/pull/630"},"issue":{"href":"https://api.github.com/repos/spotify/styx/issues/630"},"comments":{"href":"https://api.github.com/repos/spotify/styx/issues/630/comments"},"review_comments":{"href":"https://api.github.com/repos/spotify/styx/pulls/630/comments"},"review_comment":{"href":"https://api.github.com/repos/spotify/styx/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/spotify/styx/pulls/630/commits"},"statuses":{"href":"https://api.github.com/repos/spotify/styx/statuses/2faa00e3476fc0e1f17c440f19fae8e839552c66"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"honnix"}}