Skip to content
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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

bakosa
Copy link

@bakosa bakosa commented Feb 3, 2024

Description

As you can see here in the GitHub Docs you can now pass in ether a list of repos via the repositories or repository_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 and token_repository_ids arguments to the AppInstallationAuth class and uses them when invoking the get_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 both repositories and repository_ids in because 🦄

Related open issues

#2807

@bakosa bakosa changed the title Add Ability to Scope Innstall Access Tokens to a List of Repositories Add Ability to Scope Install Access Tokens to a List of Repositories Feb 3, 2024
@codecov-commenter
Copy link

codecov-commenter commented Feb 3, 2024

Codecov Report

Attention: Patch coverage is 95.00000% with 1 lines in your changes are missing coverage. Please review.

Project coverage is 96.74%. Comparing base (e4106e0) to head (f8ed209).

Files Patch % Lines
github/Auth.py 85.71% 1 Missing ⚠️

❗ 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.
📢 Have feedback on the report? Share it here.

@bakosa
Copy link
Author

bakosa commented Feb 6, 2024

Is there anything I can do to help get this merged in 😃

@mchieco
Copy link

mchieco commented Feb 13, 2024

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

github/AppAuthentication.py Outdated Show resolved Hide resolved
github/GithubIntegration.py Outdated Show resolved Hide resolved
@bakosa
Copy link
Author

bakosa commented Feb 23, 2024

This should be good to go now!

github/Auth.py Outdated
Comment on lines 191 to 193
token_permissions: Optional[Dict[str, str]] = None,
requester: Optional[Requester] = None,
token_repositories: Optional[List[Union[str, int]]] = None,
Copy link
Collaborator

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:

Suggested change
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.

Copy link
Author

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/Auth.py Outdated Show resolved Hide resolved
github/Auth.py Outdated Show resolved Hide resolved
github/Auth.py Outdated Show resolved Hide resolved
github/GithubIntegration.py Outdated Show resolved Hide resolved
repository_names = []
repository_ids = []
for r in repositories:
print(r, type(r))
Copy link
Collaborator

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

Copy link
Author

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 🤦‍♂️

Comment on lines 239 to 251
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about list comprehension?

Suggested change
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)]

Copy link
Author

@bakosa bakosa Mar 4, 2024

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!

@EnricoMi
Copy link
Collaborator

EnricoMi commented Mar 3, 2024

Note that #2912 will add some Requester-related assertions to github.Auth that might conflict with your changes.

@bakosa
Copy link
Author

bakosa commented Mar 4, 2024

Note that #2912 will add some Requester-related assertions to github.Auth that might conflict with your changes.

Sounds good, hopefully this PR is now ready! Happy to handle any conflicts if #2912 gets in first. Curious the reasoning behind all the runtime type checking?

@EnricoMi
Copy link
Collaborator

EnricoMi commented Mar 8, 2024

Merge, happy conflict chasing!

@bakosa
Copy link
Author

bakosa commented Mar 8, 2024

Resolved 🚀

Copy link
Collaborator

@EnricoMi EnricoMi left a 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!

github/GithubIntegration.py Outdated Show resolved Hide resolved
github/GithubIntegration.py Show resolved Hide resolved
github/GithubIntegration.py Outdated Show resolved Hide resolved
tests/GithubIntegration.py Show resolved Hide resolved
@EnricoMi
Copy link
Collaborator

EnricoMi commented Mar 9, 2024

I have added the new argument to the examples docs: 80407cc

) -> 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)):
Copy link
Author

@bakosa bakosa Mar 9, 2024

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..)

Suggested change
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!

Copy link
Collaborator

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()?

Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants