From 424eed52750344d0947626a2f09dce697da70992 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 | 67 +++++++++++++++++++ github/Organization.pyi | 10 +++ tests/Organization.py | 19 ++++++ .../Organization.testCreateSecret.txt | 21 ++++++ .../Organization.testCreateSecretSelected.txt | 43 ++++++++++++ .../Organization.testDeleteSecret.txt | 10 +++ 6 files changed, 170 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..f0d8052102 100644 --- a/github/Organization.py +++ b/github/Organization.py @@ -582,6 +582,49 @@ def create_repo( self._requester, headers, data, completed=True ) + def create_secret( + self, + secret_name, + unencrypted_value, + visibility="all", + selected_repositories=github.GithubObject.NotSet, + ): + """ + :calls: `PUT /orgs/{org}/actions/secrets/{secret_name} `_ + :param secret_name: string + :param unencrypted_value: string + :param visibility: string + :param selected_repositories: list of :class:`github.Repository.Repository` + :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_repositories, list) and all( + isinstance(element, github.Repository.Repository) + for element in selected_repositories + ), selected_repositories + else: + assert selected_repositories is github.GithubObject.NotSet + + 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_repositories is not github.GithubObject.NotSet: + put_parameters["selected_repository_ids"] = [ + element.id for element in selected_repositories + ] + + 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 +684,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 +967,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..6924c01edd 100644 --- a/github/Organization.pyi +++ b/github/Organization.pyi @@ -12,6 +12,7 @@ from github.NamedUser import NamedUser from github.PaginatedList import PaginatedList from github.Plan import Plan from github.Project import Project +from github.PublicKey import PublicKey from github.Repository import Repository from github.Team import Team @@ -66,6 +67,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_repositories: Union[List[Repository], _NotSetType] = ..., + ) -> bool: ... def create_team( self, name: str, @@ -79,6 +87,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 +142,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..40a3483291 100644 --- a/tests/Organization.py +++ b/tests/Organization.py @@ -33,6 +33,7 @@ ################################################################################ import datetime +from unittest import mock import github @@ -356,6 +357,24 @@ 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 + repos = [self.org.get_repo("TestPyGithub"), self.org.get_repo("FatherBeaver")] + encrypt.return_value = "M+5Fm/BqTfB90h3nC7F3BoZuu3nXs+/KtpXwxm9gG211tbRo0F5UiN0OIfYT83CKcx9oKES9Va4E96/b" + self.assertTrue( + self.org.create_secret("secret-name", "secret-value", "selected", repos) + ) + + 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..755a3804eb --- /dev/null +++ b/tests/ReplayData/Organization.testCreateSecretSelected.txt @@ -0,0 +1,43 @@ +https +GET +api.github.com +None +/repos/BeaverSoftware/TestPyGithub +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4992'), ('content-length', '1431'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4ecd2c151a469cfa6cd45e6beff1269b"'), ('date', 'Fri, 01 Jun 2012 19:40:56 GMT'), ('content-type', 'application/json; charset=utf-8')] +{"clone_url":"https://github.com/BeaverSoftware/TestPyGithub.git","has_downloads":true,"watchers":1,"git_url":"git://github.com/BeaverSoftware/TestPyGithub.git","updated_at":"2012-04-25T06:51:38Z","permissions":{"pull":true,"admin":true,"push":true},"homepage":"http://vincent-jacques.net/PyGithub","url":"https://api.github.com/repos/BeaverSoftware/TestPyGithub","has_wiki":true,"has_pages":false,"has_issues":false,"fork":false,"forks":0,"mirror_url":null,"size":112,"private":false,"open_issues":0,"svn_url":"https://github.com/BeaverSoftware/TestPyGithub","owner":{"url":"https://api.github.com/users/BeaverSoftware","gravatar_id":"d563e337cac2fdc644e2aaaad1e23266","login":"BeaverSoftware","id":1424031,"avatar_url":"https://secure.gravatar.com/avatar/d563e337cac2fdc644e2aaaad1e23266?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-orgs.png"},"name":"TestPyGithub","language":null,"description":"Guinea-pig for PyGithub testing","ssh_url":"git@github.com:BeaverSoftware/TestPyGithub.git","pushed_at":"2012-03-03T08:57:40Z","created_at":"2012-03-03T07:53:19Z","id":3609352,"html_url":"https://github.com/BeaverSoftware/TestPyGithub","full_name":"BeaverSoftware/TestPyGithub"} + +https +GET +api.github.com +None +/repos/BeaverSoftware/FatherBeaver +{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'} +None +200 +[('status', '200 OK'), ('x-ratelimit-remaining', '4992'), ('content-length', '1431'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4ecd2c151a469cfa6cd45e6beff1269b"'), ('date', 'Fri, 01 Jun 2012 19:40:56 GMT'), ('content-type', 'application/json; charset=utf-8')] +{"clone_url":"https://github.com/BeaverSoftware/FatherBeaver.git","has_downloads":true,"watchers":2,"git_url":"git://github.com/BeaverSoftware/FatherBeaver.git","updated_at":"2012-02-16T21:51:15Z","permissions":{"pull":true,"admin":true,"push":true},"homepage":"","url":"https://api.github.com/repos/BeaverSoftware/FatherBeaver","has_wiki":true,"has_pages":true,"has_issues":true,"fork":false,"forks":1,"mirror_url":null,"size":0,"private":false,"open_issues":0,"svn_url":"https://github.com/BeaverSoftware/FatherBeaver","owner":{"url":"https://api.github.com/users/BeaverSoftware","gravatar_id":"d563e337cac2fdc644e2aaaad1e23266","login":"BeaverSoftware","id":1424031,"avatar_url":"https://secure.gravatar.com/avatar/d563e337cac2fdc644e2aaaad1e23266?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-orgs.png"},"name":"FatherBeaver","language":null,"description":"","ssh_url":"git@github.com:BeaverSoftware/FatherBeaver.git","pushed_at":null,"created_at":"2012-02-09T19:32:21Z","id":3400397,"html_url":"https://github.com/BeaverSoftware/FatherBeaver","full_name":"BeaverSoftware/FatherBeaver"} + +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": [3609352, 3400397]} +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