New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Ability to Scope Install Access Tokens to a List of Repositories #2892
base: main
Are you sure you want to change the base?
Conversation
…s to get install access token
Codecov ReportAttention: Patch coverage is
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #2892 +/- ##
==========================================
- Coverage 96.74% 96.74% -0.01%
==========================================
Files 147 147
Lines 14978 14993 +15
==========================================
+ Hits 14491 14505 +14
- Misses 487 488 +1 ☔ View full report in Codecov by Sentry. |
Is there anything I can do to help get this merged in 😃 |
Any updates on this issue/PR? Was looking around for this functionality and if we could get this merged in that would be great so I don't have to fork for this feature |
This should be good to go now! |
github/Auth.py
Outdated
token_permissions: Optional[Dict[str, str]] = None, | ||
requester: Optional[Requester] = None, | ||
token_repositories: Optional[List[Union[str, int]]] = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though this method is public, the requester
argument is internal in a sense that there should be no user code calling this method with requester
given. Hence, it is safe (and preferable) to add token_repositories
before requester
:
token_permissions: Optional[Dict[str, str]] = None, | |
requester: Optional[Requester] = None, | |
token_repositories: Optional[List[Union[str, int]]] = None, | |
token_permissions: Optional[Dict[str, str]] = None, | |
token_repositories: Optional[List[Union[str, int]]] = None, | |
*, | |
requester: Optional[Requester] = None, |
Adding the *
makes the requester
argument mandatory named, which avoids breaking user code in the future when adding an argument before it.
This requires code that provides requester
to add requester=...
throughout the project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering the same thing when I went to add the new argument. This makes sense! thanks for the deeper explanation.
github/GithubIntegration.py
Outdated
repository_names = [] | ||
repository_ids = [] | ||
for r in repositories: | ||
print(r, type(r)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this debug line should be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, that is just embarrassing 🤦♂️
github/GithubIntegration.py
Outdated
repository_names = [] | ||
repository_ids = [] | ||
for r in repositories: | ||
print(r, type(r)) | ||
if isinstance(r, str): | ||
repository_names.append(r) | ||
elif isinstance(r, int): | ||
repository_ids.append(r) | ||
|
||
if repository_names: | ||
body["repositories"] = repository_names | ||
if repository_ids: | ||
body["repository_ids"] = repository_ids |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about list comprehension?
repository_names = [] | |
repository_ids = [] | |
for r in repositories: | |
print(r, type(r)) | |
if isinstance(r, str): | |
repository_names.append(r) | |
elif isinstance(r, int): | |
repository_ids.append(r) | |
if repository_names: | |
body["repositories"] = repository_names | |
if repository_ids: | |
body["repository_ids"] = repository_ids | |
if repository_names: | |
body["repositories"] = [r for r in repositories if isinstance(r, str)] | |
if repository_ids: | |
body["repository_ids"] = [r for r in repositories if isinstance(r, int)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not originally use list comprehension in favor of only iterating over repositories
once to type check. Also, need to null check before adding to the body.
However, my latest commit swaps it over to using list comprehension, I suppose it is still more readable!
Note that #2912 will add some |
Merge, happy conflict chasing! |
Resolved 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor changes, LGTM overall!
I have added the new argument to the examples docs: 80407cc |
github/GithubIntegration.py
Outdated
) -> InstallationAuthorization: | ||
""" | ||
:calls: `POST /app/installations/{installation_id}/access_tokens <https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app>` | ||
""" | ||
if permissions is None: | ||
permissions = {} | ||
|
||
if repositories is not None and not (isinstance(repositories, list) and all(isinstance(r, (str, int)) for r in repositories)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unit test fails here because in Python, booleans are subclass of integers 🙃
We could ether be explicit about the type check (exclude subclasses..)
if repositories is not None and not (isinstance(repositories, list) and all(isinstance(r, (str, int)) for r in repositories)): | |
if repositories is not None and not (isinstance(repositories, list) and all(type(r) in (str, int) for r in repositories)): |
Or change the unit test. Not sure why someone would pass in a subclassed object or str or int into the list of repositories but I will leave it up to you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's stick to isinstance
as we use it everywhere while it is not accurate, as you pointed out. Can you fix the unittest by simply using object()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, updated the test.
…ting access token
Description
As you can see here in the GitHub Docs you can now pass in ether a list of repos via the
repositories
orrepository_ids
body parameters when you create an app installation access token.The problem that this is trying to solve is we need to generate least privileged tokens scoped to only specific repositories and specific permissions.
This PR adds new
token_repositories
andtoken_repository_ids
arguments to theAppInstallationAuth
class and uses them when invoking theget_access_token
method to get a scoped app install access token.Things to Note
I tired to follow the same pattern that is being used for the
permissions
body parameter. Instead of defaulting to{}
when nothing is passed in, None is preferred here since that will translate to null which drive the expected outcome (tested this). Also, you can pass a combination of bothrepositories
andrepository_ids
in because 🦄Related open issues
#2807