From ccf8233503226d274bf58437fa62614f37eb10fa Mon Sep 17 00:00:00 2001 From: Marina Peresypkina Date: Thu, 22 Jul 2021 15:38:18 +0300 Subject: [PATCH] Add github actions secrets to org --- github/Organization.py | 60 +++++++++++++++++++ github/Organization.pyi | 9 +++ tests/Organization.py | 20 +++++++ .../Organization.testCreateSecret.txt | 21 +++++++ .../Organization.testCreateSecretSelected.txt | 21 +++++++ .../Organization.testDeleteSecret.txt | 10 ++++ 6 files changed, 141 insertions(+) create mode 100644 tests/ReplayData/Organization.testCreateSecret.txt create mode 100644 tests/ReplayData/Organization.testCreateSecretSelected.txt create mode 100644 tests/ReplayData/Organization.testDeleteSecret.txt diff --git a/github/Organization.py b/github/Organization.py index 0248945430..eedcd9a945 100644 --- a/github/Organization.py +++ b/github/Organization.py @@ -582,6 +582,42 @@ def create_repo( self._requester, headers, data, completed=True ) + def create_secret( + self, + secret_name, + unencrypted_value, + visibility="all", + selected_repository_ids=[], + ): + """ + :calls: `PUT /orgs/{org}/actions/secrets/{secret_name} `_ + :param secret_name: string + :param unencrypted_value: string + :param visibility: string + :param selected_repository_ids: list + :rtype: bool + """ + assert isinstance(secret_name, str), secret_name + assert isinstance(unencrypted_value, str), unencrypted_value + assert isinstance(visibility, str), visibility + if visibility == "selected": + assert isinstance(selected_repository_ids, list) + + public_key = self.get_public_key() + payload = public_key.encrypt(unencrypted_value) + put_parameters = { + "key_id": public_key.key_id, + "encrypted_value": payload, + "visibility": visibility, + } + if selected_repository_ids: + put_parameters["selected_repository_ids"] = selected_repository_ids + + status, headers, data = self._requester.requestJson( + "PUT", f"{self.url}/actions/secrets/{secret_name}", input=put_parameters + ) + return status == 201 + def create_team( self, name, @@ -641,6 +677,18 @@ def delete_hook(self, id): "DELETE", f"{self.url}/hooks/{id}" ) + def delete_secret(self, secret_name): + """ + :calls: `DELETE /orgs/{org}/actions/secrets/{secret_name} `_ + :param secret_name: string + :rtype: bool + """ + assert isinstance(secret_name, str), secret_name + status, headers, data = self._requester.requestJson( + "DELETE", f"{self.url}/actions/secrets/{secret_name}" + ) + return status == 204 + def edit( self, billing_email=github.GithubObject.NotSet, @@ -912,6 +960,18 @@ def convert_to_outside_collaborator(self, member): "PUT", f"{self.url}/outside_collaborators/{member._identity}" ) + def get_public_key(self): + """ + :calls: `GET /orgs/{org}/actions/secrets/public-key `_ + :rtype: :class:`github.PublicKey.PublicKey` + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", f"{self.url}/actions/secrets/public-key" + ) + return github.PublicKey.PublicKey( + self._requester, headers, data, completed=True + ) + def get_repo(self, name): """ :calls: `GET /repos/{owner}/{repo} `_ diff --git a/github/Organization.pyi b/github/Organization.pyi index cd5f2086e4..bb8a4e7594 100644 --- a/github/Organization.pyi +++ b/github/Organization.pyi @@ -66,6 +66,13 @@ class Organization(CompletableGithubObject): allow_merge_commit: Union[bool, _NotSetType] = ..., allow_rebase_merge: Union[bool, _NotSetType] = ..., ) -> Repository: ... + def create_secret( + self, + secret_name: str, + unencrypted_value: str, + visibility: str, + selected_repository_ids: list, + ) -> bool: ... def create_team( self, name: str, @@ -79,6 +86,7 @@ class Organization(CompletableGithubObject): def delete_hook(self, id: int) -> None: ... @property def default_repository_permission(self) -> str: ... + def delete_secret(self, secret_name: str) -> bool: ... @property def description(self) -> str: ... @property @@ -133,6 +141,7 @@ class Organization(CompletableGithubObject): def get_projects( self, state: Union[_NotSetType, str] = ... ) -> PaginatedList[Project]: ... + def get_public_key(self) -> PublicKey: ... def get_public_members(self) -> PaginatedList[NamedUser]: ... def get_repo(self, name: str) -> Repository: ... def get_repos( diff --git a/tests/Organization.py b/tests/Organization.py index de59a52383..1a29688d8d 100644 --- a/tests/Organization.py +++ b/tests/Organization.py @@ -33,6 +33,7 @@ ################################################################################ import datetime +from unittest import mock import github @@ -356,6 +357,25 @@ def testCreateFork(self): self.assertFalse(repo.has_wiki) self.assertFalse(repo.has_pages) + @mock.patch("github.PublicKey.encrypt") + def testCreateSecret(self, encrypt): + # encrypt returns a non-deterministic value, we need to mock it so the replay data matches + encrypt.return_value = "M+5Fm/BqTfB90h3nC7F3BoZuu3nXs+/KtpXwxm9gG211tbRo0F5UiN0OIfYT83CKcx9oKES9Va4E96/b" + self.assertTrue( + self.org.create_secret("secret-name", "secret-value", "all") + ) + + @mock.patch("github.PublicKey.encrypt") + def testCreateSecretSelected(self, encrypt): + # encrypt returns a non-deterministic value, we need to mock it so the replay data matches + encrypt.return_value = "M+5Fm/BqTfB90h3nC7F3BoZuu3nXs+/KtpXwxm9gG211tbRo0F5UiN0OIfYT83CKcx9oKES9Va4E96/b" + self.assertTrue( + self.org.create_secret("secret-name", "secret-value", "selected", ["3544490"]) + ) + + def testDeleteSecret(self): + self.assertTrue(self.org.delete_secret("secret-name")) + def testInviteUserWithNeither(self): with self.assertRaises(AssertionError) as raisedexp: self.org.invite_user() diff --git a/tests/ReplayData/Organization.testCreateSecret.txt b/tests/ReplayData/Organization.testCreateSecret.txt new file mode 100644 index 0000000000..3188f76c16 --- /dev/null +++ b/tests/ReplayData/Organization.testCreateSecret.txt @@ -0,0 +1,21 @@ +https +GET +api.github.com +None +/orgs/BeaverSoftware/actions/secrets/public-key +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4978'), ('content-length', '487'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"1dd282b50e691f8f162ef9355dad8771"'), ('date', 'Thu, 10 May 2012 19:03:19 GMT'), ('content-type', 'application/json; charset=utf-8')] +{"key": "u5e1Z25+z8pmgVVt5Pd8k0z/sKpVL1MXYtRAecE4vm8=", "key_id": "568250167242549743"} + +https +PUT +api.github.com +None +/orgs/BeaverSoftware/actions/secrets/secret-name +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"encrypted_value": "M+5Fm/BqTfB90h3nC7F3BoZuu3nXs+/KtpXwxm9gG211tbRo0F5UiN0OIfYT83CKcx9oKES9Va4E96/b", "key_id": "568250167242549743", "visibility": "all"} +201 +[('Date', 'Fri, 17 Apr 2020 00:12:33 GMT'), ('Server', 'GitHub.com'), ('Content-Length', '2'), ('Content-Type', 'application/json; charset=utf-8'), ('Status', '201 Created'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4984'), ('X-RateLimit-Reset', '1587085388'), ('X-OAuth-Scopes', 'read:org, repo, user'), ('X-Accepted-OAuth-Scopes', ''), ('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-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, 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', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Vary', 'Accept-Encoding, Accept, X-Requested-With'), ('X-GitHub-Request-Id', 'C290:52DA:50234:B404B:5E98F470')] +{} \ No newline at end of file diff --git a/tests/ReplayData/Organization.testCreateSecretSelected.txt b/tests/ReplayData/Organization.testCreateSecretSelected.txt new file mode 100644 index 0000000000..292b31244a --- /dev/null +++ b/tests/ReplayData/Organization.testCreateSecretSelected.txt @@ -0,0 +1,21 @@ +https +GET +api.github.com +None +/orgs/BeaverSoftware/actions/secrets/public-key +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4978'), ('content-length', '487'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"1dd282b50e691f8f162ef9355dad8771"'), ('date', 'Thu, 10 May 2012 19:03:19 GMT'), ('content-type', 'application/json; charset=utf-8')] +{"key": "u5e1Z25+z8pmgVVt5Pd8k0z/sKpVL1MXYtRAecE4vm8=", "key_id": "568250167242549743"} + +https +PUT +api.github.com +None +/orgs/BeaverSoftware/actions/secrets/secret-name +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'} +{"encrypted_value": "M+5Fm/BqTfB90h3nC7F3BoZuu3nXs+/KtpXwxm9gG211tbRo0F5UiN0OIfYT83CKcx9oKES9Va4E96/b", "key_id": "568250167242549743", "visibility": "selected", "selected_repository_ids": ["3544490"]} +201 +[('Date', 'Fri, 17 Apr 2020 00:12:33 GMT'), ('Server', 'GitHub.com'), ('Content-Length', '2'), ('Content-Type', 'application/json; charset=utf-8'), ('Status', '201 Created'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4984'), ('X-RateLimit-Reset', '1587085388'), ('X-OAuth-Scopes', 'read:org, repo, user'), ('X-Accepted-OAuth-Scopes', ''), ('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-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, 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', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Vary', 'Accept-Encoding, Accept, X-Requested-With'), ('X-GitHub-Request-Id', 'C290:52DA:50234:B404B:5E98F470')] +{} \ No newline at end of file diff --git a/tests/ReplayData/Organization.testDeleteSecret.txt b/tests/ReplayData/Organization.testDeleteSecret.txt new file mode 100644 index 0000000000..cee0237a44 --- /dev/null +++ b/tests/ReplayData/Organization.testDeleteSecret.txt @@ -0,0 +1,10 @@ +https +DELETE +api.github.com +None +/orgs/BeaverSoftware/actions/secrets/secret-name +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +204 +[('Date', 'Fri, 17 Apr 2020 00:12:33 GMT'), ('Server', 'GitHub.com'), ('Content-Length', '2'), ('Content-Type', 'application/json; charset=utf-8'), ('Status', '201 Created'), ('X-RateLimit-Limit', '5000'), ('X-RateLimit-Remaining', '4984'), ('X-RateLimit-Reset', '1587085388'), ('X-OAuth-Scopes', 'read:org, repo, user'), ('X-Accepted-OAuth-Scopes', ''), ('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-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, 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', '1; mode=block'), ('Referrer-Policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('Content-Security-Policy', "default-src 'none'"), ('Vary', 'Accept-Encoding, Accept, X-Requested-With'), ('X-GitHub-Request-Id', 'C290:52DA:50234:B404B:5E98F470')] +{} \ No newline at end of file