diff --git a/doc/examples/Authentication.rst b/doc/examples/Authentication.rst index 685c1650..b707d2ed 100644 --- a/doc/examples/Authentication.rst +++ b/doc/examples/Authentication.rst @@ -75,6 +75,16 @@ expiration timeout. The access token is refreshed automatically. >>> g.get_repo("user/repo").name 'repo' +Alternatively, the `github.Github` instance can be retrieved via `github.GithubIntegration`: + +.. code-block:: python + + >>> auth = Auth.AppAuth(123456, private_key) + >>> gi = GithubIntegration(auth=auth) + >>> g = gi.get_github_for_installation(installation_id, token_permissions) + >>> g.get_repo("user/repo").name + 'repo' + App user authentication ----------------------- diff --git a/github/Auth.py b/github/Auth.py index e6c0d559..44469790 100644 --- a/github/Auth.py +++ b/github/Auth.py @@ -253,10 +253,7 @@ class AppInstallationAuth(Auth, WithRequester["AppInstallationAuth"]): from github.GithubIntegration import GithubIntegration - self.__integration = GithubIntegration( - auth=self._app_auth, - base_url=requester.base_url, - ) + self.__integration = GithubIntegration(**self.requester.kwargs) return self diff --git a/github/GithubIntegration.py b/github/GithubIntegration.py index 29bccc54..babba784 100644 --- a/github/GithubIntegration.py +++ b/github/GithubIntegration.py @@ -119,9 +119,9 @@ class GithubIntegration: seconds_between_writes=seconds_between_writes, ) - def get_github_for_installation(self, installation_id): + def get_github_for_installation(self, installation_id, token_permissions=None): # The installation has to authenticate as an installation, not an app - auth = self.auth.get_installation_auth(installation_id, requester=self.__requester) + auth = self.auth.get_installation_auth(installation_id, token_permissions, self.__requester) return github.Github(**self.__requester.withAuth(auth).kwargs) def _get_headers(self): diff --git a/github/GithubIntegration.pyi b/github/GithubIntegration.pyi index b4cf1e83..1b3383ee 100644 --- a/github/GithubIntegration.pyi +++ b/github/GithubIntegration.pyi @@ -32,7 +32,9 @@ class GithubIntegration: jwt_algorithm: str = ..., auth: Optional[AppAuth] = ..., ) -> None: ... - def get_github_for_installation(self, installation_id: int) -> github.Github: ... + def get_github_for_installation( + self, installation_id: int, token_permissions: Optional[Dict[str, str]] = ... + ) -> github.Github: ... def _get_installed_app(self, url: str) -> Installation: ... def _get_headers(self) -> Dict[str, str]: ... def create_jwt(self, expiration: Optional[int] = ...) -> str: ... diff --git a/github/MainClass.py b/github/MainClass.py index b9707c07..d3010c2a 100644 --- a/github/MainClass.py +++ b/github/MainClass.py @@ -815,7 +815,7 @@ class Github: "github.GithubIntegration(auth=github.Auth.AppAuth(...)).get_app() instead", category=DeprecationWarning, ) - return GithubIntegration(auth=self.__requester.auth).get_app() + return GithubIntegration(**self.__requester.kwargs).get_app() else: # with a slug given, we can lazily load the GithubApp return GithubApp.GithubApp(self.__requester, {}, {"url": f"/apps/{slug}"}, completed=False) diff --git a/tests/Authentication.py b/tests/Authentication.py index 6a477851..555296eb 100644 --- a/tests/Authentication.py +++ b/tests/Authentication.py @@ -97,9 +97,64 @@ class Authentication(Framework.BasicTestCase): def testAppAuthTokenAuthentication(self): # test data copied from testJWTAuthentication to test parity - g = github.Github(auth=self.app_auth) + g = github.Github(auth=self.jwt) self.assertEqual(g.get_user("jacquev6").name, "Vincent Jacques") + def testAppAuthAuthentication(self): + # test data copied from testAppAuthentication to test parity + g = github.Github(auth=self.app_auth.get_installation_auth(29782936)) + self.assertEqual(g.get_user("ammarmallik").name, "Ammar Akbar") + + def assert_requester_args(self, g, expected_requester): + expected_args = expected_requester.kwargs + expected_args.pop("auth") + + auth_args = g._Github__requester.auth.requester.kwargs + auth_args.pop("auth") + + self.assertEqual(expected_args, auth_args) + + auth_integration_args = ( + g._Github__requester.auth._AppInstallationAuth__integration._GithubIntegration__requester.kwargs + ) + auth_integration_args.pop("auth") + + self.assertEqual(expected_args, auth_integration_args) + + def testAppAuthAuthenticationWithGithubRequesterArgs(self): + # test that Requester arguments given to github.Github are passed to auth and auth.__integration + g = github.Github( + auth=self.app_auth.get_installation_auth(29782936), + base_url="https://base.net/", + timeout=60, + user_agent="agent", + per_page=100, + verify="cert", + retry=999, + pool_size=10, + seconds_between_requests=100, + seconds_between_writes=1000, + ) + + self.assert_requester_args(g, g._Github__requester) + + def testAppAuthAuthenticationWithGithubIntegrationRequesterArgs(self): + # test that Requester arguments given to github.GithubIntegration are passed to auth and auth.__integration + gi = github.GithubIntegration( + auth=self.app_auth, + base_url="https://base.net/", + timeout=60, + user_agent="agent", + per_page=100, + verify="cert", + retry=999, + pool_size=10, + seconds_between_requests=100, + seconds_between_writes=1000, + ) + + self.assert_requester_args(gi.get_github_for_installation(29782936), gi._GithubIntegration__requester) + def testAppInstallationAuthAuthentication(self): # test data copied from testAppAuthentication to test parity installation_auth = github.Auth.AppInstallationAuth(self.app_auth, 29782936) @@ -137,6 +192,12 @@ class Authentication(Framework.BasicTestCase): self.assertEqual(g.get_user("ammarmallik").name, "Ammar Akbar") self.assertEqual(g.get_repo("PyGithub/PyGithub").full_name, "PyGithub/PyGithub") + def testAppInstallationAuthAuthenticationRequesterArgs(self): + installation_auth = github.Auth.AppInstallationAuth(self.app_auth, 29782936) + github.Github( + auth=installation_auth, + ) + def testAppUserAuthentication(self): client_id = "removed client id" client_secret = "removed client secret" diff --git a/tests/GithubIntegration.py b/tests/GithubIntegration.py index ddec1c1e..4f061e88 100644 --- a/tests/GithubIntegration.py +++ b/tests/GithubIntegration.py @@ -91,28 +91,32 @@ class GithubIntegration(Framework.BasicTestCase): # with verify=False, urllib3.connectionpool rightly may issue an InsecureRequestWarning # we ignore InsecureRequestWarning from urllib3.connectionpool with self.ignoreWarning(category=InsecureRequestWarning, module="urllib3.connectionpool"): - auth = github.Auth.AppAuth(APP_ID, PRIVATE_KEY) - github_integration = github.GithubIntegration( - auth=auth, - base_url="https://api.github.com", + kwargs = dict( + auth=github.Auth.AppAuth(APP_ID, PRIVATE_KEY), + # http protocol used to deviate from default base url, recording data might require https + base_url="http://api.github.com", timeout=Consts.DEFAULT_TIMEOUT + 10, user_agent="PyGithub/Python-Test", per_page=Consts.DEFAULT_PER_PAGE + 10, verify=False, retry=3, pool_size=10, + seconds_between_requests=100, + seconds_between_writes=1000, ) + # assert kwargs consists of ALL requester constructor arguments + self.assertEqual(kwargs.keys(), github.Requester.Requester.__init__.__annotations__.keys()) + + github_integration = github.GithubIntegration(**kwargs) g = github_integration.get_github_for_installation(36541767) self.assertIsInstance(g._Github__requester.auth, AppInstallationAuth) - self.assertEqual(g._Github__requester._Requester__base_url, "https://api.github.com") - self.assertEqual(g._Github__requester._Requester__timeout, Consts.DEFAULT_TIMEOUT + 10) - self.assertEqual(g._Github__requester._Requester__userAgent, "PyGithub/Python-Test") - self.assertEqual(g._Github__requester.per_page, Consts.DEFAULT_PER_PAGE + 10) - self.assertEqual(g._Github__requester._Requester__verify, False) - self.assertEqual(g._Github__requester._Requester__retry, 3) - self.assertEqual(g._Github__requester._Requester__pool_size, 10) + + actual = g._Github__requester.kwargs + kwargs.update(auth=str(AppInstallationAuth)) + actual.update(auth=str(type(actual["auth"]))) + self.assertDictEqual(kwargs, actual) repo = g.get_repo("PyGithub/PyGithub") self.assertEqual(repo.full_name, "PyGithub/PyGithub") diff --git a/tests/Installation.py b/tests/Installation.py index c32c41fb..3c8dc6d8 100644 --- a/tests/Installation.py +++ b/tests/Installation.py @@ -50,30 +50,35 @@ class Installation(Framework.BasicTestCase): # with verify=False, urllib3.connectionpool rightly may issue an InsecureRequestWarning # we ignore InsecureRequestWarning from urllib3.connectionpool with self.ignoreWarning(category=InsecureRequestWarning, module="urllib3.connectionpool"): - self.auth = AppAuth(319953, GithubIntegration.PRIVATE_KEY) - self.integration = github.GithubIntegration( - auth=self.auth, - base_url="https://api.github.com", + kwargs = dict( + auth=AppAuth(319953, GithubIntegration.PRIVATE_KEY), + # http protocol used to deviate from default base url, recording data might require https + base_url="http://api.github.com", timeout=Consts.DEFAULT_TIMEOUT + 10, user_agent="PyGithub/Python-Test", per_page=Consts.DEFAULT_PER_PAGE + 10, verify=False, retry=3, pool_size=10, + seconds_between_requests=100, + seconds_between_writes=1000, ) + + # assert kwargs consists of ALL requester constructor arguments + self.assertEqual(kwargs.keys(), github.Requester.Requester.__init__.__annotations__.keys()) + + self.integration = github.GithubIntegration(**kwargs) installations = list(self.integration.get_installations()) installation = installations[0] g = installation.get_github_for_installation() self.assertIsInstance(g._Github__requester.auth, AppInstallationAuth) - self.assertEqual(g._Github__requester._Requester__base_url, "https://api.github.com") - self.assertEqual(g._Github__requester._Requester__timeout, Consts.DEFAULT_TIMEOUT + 10) - self.assertEqual(g._Github__requester._Requester__userAgent, "PyGithub/Python-Test") - self.assertEqual(g._Github__requester.per_page, Consts.DEFAULT_PER_PAGE + 10) - self.assertEqual(g._Github__requester._Requester__verify, False) - self.assertEqual(g._Github__requester._Requester__retry, 3) - self.assertEqual(g._Github__requester._Requester__pool_size, 10) + + actual = g._Github__requester.kwargs + kwargs.update(auth=str(AppInstallationAuth)) + actual.update(auth=str(type(actual["auth"]))) + self.assertDictEqual(kwargs, actual) repo = g.get_repo("PyGithub/PyGithub") self.assertEqual(repo.full_name, "PyGithub/PyGithub") diff --git a/tests/ReplayData/Authentication.testAppAuthAuthentication.txt b/tests/ReplayData/Authentication.testAppAuthAuthentication.txt new file mode 100644 index 00000000..9f2a6b2e --- /dev/null +++ b/tests/ReplayData/Authentication.testAppAuthAuthentication.txt @@ -0,0 +1,21 @@ +https +POST +api.github.com +None +/app/installations/29782936/access_tokens +{'Authorization': 'Bearer jwt_removed', 'Accept': 'application/vnd.github.machine-man-preview+json', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"permissions": {}} +201 +[('status', '201 CREATED'), ('server', 'Github.com'), ('date', 'Mon, 24 Oct 2022 23:11:45 GMT'), ('content-type', 'application/json; charset=utf-8'), ('connection', 'keep-alive'), ('content-length', '1962'), ('etag', 'W/"b11a1c9caabe35f1de0a13e597a3022d27d2bff0694c2ccb5a65edc3b4d18837"'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '0'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-github-request-id', "E475:53DD:8B7A89E:11E38A79:63571BB0"), ('vary', 'Accept-Encoding, Accept, X-Requested-With'), ('content-security-policy', "default-src 'none'")] +{"token":"ghs_1llwuELtXN5HDOB99XhpcTXdJxbOuF0ZlSmj", "expires_at":"2024-11-25T01:00:02Z", "permissions":{"issues":"read","metadata":"read"}, "repository_selection":"selected"} + +https +GET +api.github.com +None +/users/ammarmallik +{'Authorization': 'token private_token_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '57'), ('content-length', '1338'), ('server', 'GitHub.com'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '60'), ('etag', 'W/"e5e65462690241eb7d9bc213cc00b3df2c75984d29a36b2ee8e151deaf4f3981"'), ('date', 'Tue, 25 Oct 2022 02:01:06 GMT'), ('x-ratelimit-reset', '1666666583'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-resource', 'core'), ('x-ratelimit-used', '3'), ('accept-ranges', 'bytes'), ('x-github-request-id', 'D8C1:7CE1:C3DF20:D546B4:63574361'), ('content-security-policy', "default-src 'none'"), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('x-xss-protection', '0'), ('x-content-type-options', 'nosniff'), ('x-frame-options', 'deny'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('access-control-allow-origin', '*'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('x-github-media-type', 'github.v3; format=jsom'), ('last-modified', 'Mon, 24 Oct 2022 20:26:22 GMT'), ('cache-control', 'public, max-age=60, s-maxage=60'), ('vary', 'Accept, Accept-Encoding, Accept, X-Requested-With')] +{"login":"ammarmallik","id":29196434,"node_id":"MDQ6VXNlcjI5MTk2NDM0","avatar_url":"https://avatars.githubusercontent.com/u/29196434?v=4","gravatar_id":"","url":"https://api.github.com/users/ammarmallik","html_url":"https://github.com/ammarmallik","followers_url":"https://api.github.com/users/ammarmallik/followers","following_url":"https://api.github.com/users/ammarmallik/following{/other_user}","gists_url":"https://api.github.com/users/ammarmallik/gists{/gist_id}","starred_url":"https://api.github.com/users/ammarmallik/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ammarmallik/subscriptions","organizations_url":"https://api.github.com/users/ammarmallik/orgs","repos_url":"https://api.github.com/users/ammarmallik/repos","events_url":"https://api.github.com/users/ammarmallik/events{/privacy}","received_events_url":"https://api.github.com/users/ammarmallik/received_events","type":"User","site_admin":false,"name":"Ammar Akbar","company":null,"blog":"","location":"Lahore","email":null,"hireable":true,"bio":null,"twitter_username":null,"public_repos":14,"public_gists":2,"followers":0,"following":3,"created_at":"2017-06-05T09:42:01Z","updated_at":"2022-10-24T20:26:22Z"} diff --git a/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt b/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt index 7cdc4ca1..8ec88664 100644 --- a/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt +++ b/tests/ReplayData/GithubIntegration.testGetGithubForInstallation.txt @@ -1,15 +1,15 @@ -https +http POST api.github.com None /app/installations/36541767/access_tokens -{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python-Test', 'Content-Type': 'application/json'} {"permissions": {}} 201 [('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 07:14:58 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Content-Length', '231'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', '"04bc8f50376b119ec74528a85ed2ca423635d43147beced32e7285a87ee752b4"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('X-GitHub-Request-Id', 'D2F2:647B:BB00E92:BD3FE86:64900071')] {"token":"private_token_removed","expires_at":"2023-06-19T08:14:58Z","permissions":{"metadata":"read"},"repository_selection":"selected"} -https +http GET api.github.com None diff --git a/tests/ReplayData/Installation.testGetGithubForInstallation.txt b/tests/ReplayData/Installation.testGetGithubForInstallation.txt index 0f9d3218..b995c579 100644 --- a/tests/ReplayData/Installation.testGetGithubForInstallation.txt +++ b/tests/ReplayData/Installation.testGetGithubForInstallation.txt @@ -1,4 +1,4 @@ -https +http GET api.github.com None @@ -9,18 +9,18 @@ None [('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 06:59:50 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Transfer-Encoding', 'chunked'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', 'W/"b272db57fded7547562b4a357681c6c84054e8fb5169b32206e833bbe7f542e5"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Content-Encoding', 'gzip'), ('X-GitHub-Request-Id', 'B368:A85B:CEF8FE3:D136340:648FFCE5')] [{"id":36541767,"account":{"login":"EnricoMi","id":44700269,"node_id":"MDQ6VXNlcjQ0NzAwMjY5","avatar_url":"https://avatars.githubusercontent.com/u/44700269?v=4","gravatar_id":"","url":"https://api.github.com/users/EnricoMi","html_url":"https://github.com/EnricoMi","followers_url":"https://api.github.com/users/EnricoMi/followers","following_url":"https://api.github.com/users/EnricoMi/following{/other_user}","gists_url":"https://api.github.com/users/EnricoMi/gists{/gist_id}","starred_url":"https://api.github.com/users/EnricoMi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/EnricoMi/subscriptions","organizations_url":"https://api.github.com/users/EnricoMi/orgs","repos_url":"https://api.github.com/users/EnricoMi/repos","events_url":"https://api.github.com/users/EnricoMi/events{/privacy}","received_events_url":"https://api.github.com/users/EnricoMi/received_events","type":"User","site_admin":false},"repository_selection":"selected","access_tokens_url":"https://api.github.com/app/installations/36541767/access_tokens","repositories_url":"https://api.github.com/installation/repositories","html_url":"https://github.com/settings/installations/36541767","app_id":319953,"app_slug":"publish-test-results","target_id":44700269,"target_type":"User","permissions":{"checks":"write","issues":"read","contents":"read","metadata":"read","pull_requests":"write"},"events":[],"created_at":"2023-04-17T16:18:05.000Z","updated_at":"2023-06-08T07:38:12.000Z","single_file_name":null,"has_multiple_single_files":false,"single_file_paths":[],"suspended_by":null,"suspended_at":null}] -https +http POST api.github.com None /app/installations/36541767/access_tokens -{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{'Accept': 'application/vnd.github.machine-man-preview+json', 'Authorization': 'Bearer jwt_removed', 'User-Agent': 'PyGithub/Python-Test', 'Content-Type': 'application/json'} {"permissions": {}} 201 [('Server', 'GitHub.com'), ('Date', 'Mon, 19 Jun 2023 06:59:50 GMT'), ('Content-Type', 'application/json; charset=utf-8'), ('Content-Length', '231'), ('Cache-Control', 'public, max-age=60, s-maxage=60'), ('Vary', 'Accept, Accept-Encoding, Accept, X-Requested-With'), ('ETag', '"056522667824941d4f276f2d2051ea5c4fd160bd6be7da7765aa63926fc99593"'), ('X-GitHub-Media-Type', 'github.v3; param=machine-man-preview; format=json'), ('x-github-api-version-selected', '2022-11-28'), ('Access-Control-Expose-Headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset'), ('Access-Control-Allow-Origin', '*'), ('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'), ('X-Frame-Options', 'deny'), ('X-Content-Type-Options', 'nosniff'), ('X-XSS-Protection', '0'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('X-GitHub-Request-Id', 'B370:F0F6:D6213DB:D85E7E7:648FFCE6')] {"token":"private_token_removed","expires_at":"2023-06-19T07:59:50Z","permissions":{"metadata":"read"},"repository_selection":"selected"} -https +http GET api.github.com None