Skip to content

Commit

Permalink
Add github actions secrets to org (PyGithub#2006)
Browse files Browse the repository at this point in the history
Add functionality to create and delete Secrets for Organizations.
  • Loading branch information
peresypkinamarina committed Oct 13, 2021
1 parent fcc1236 commit bc5e595
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 0 deletions.
67 changes: 67 additions & 0 deletions github/Organization.py
Expand Up @@ -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} <https://docs.github.com/en/rest/reference/actions#create-or-update-an-organization-secret>`_
: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,
Expand Down Expand Up @@ -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} <https://docs.github.com/en/rest/reference/actions#delete-an-organization-secret>`_
: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,
Expand Down Expand Up @@ -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 <https://docs.github.com/en/rest/reference/actions#get-an-organization-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} <https://docs.github.com/en/rest/reference/repos>`_
Expand Down
10 changes: 10 additions & 0 deletions github/Organization.pyi
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 19 additions & 0 deletions tests/Organization.py
Expand Up @@ -33,6 +33,7 @@
################################################################################

import datetime
from unittest import mock

import github

Expand Down Expand Up @@ -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()
Expand Down
21 changes: 21 additions & 0 deletions 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')]
{}
43 changes: 43 additions & 0 deletions 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')]
{}
10 changes: 10 additions & 0 deletions 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')]
{}

0 comments on commit bc5e595

Please sign in to comment.