-
Notifications
You must be signed in to change notification settings - Fork 240
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
feat: Add oauth flow for querybook github integration #1497
Merged
Merged
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
5a6db84
feat: Add oauth flow for querybook github integration
zhangvi7 27dbd01
address comments
zhangvi7 9932e31
link datadoc to github directory
zhangvi7 18bd8b3
3.35.0
zhangvi7 daa18d2
alembic migration
zhangvi7 bcede5d
feat: Add Datadoc serializing util
zhangvi7 88cc1ff
fix serialization bug and add unit test
zhangvi7 398cb62
Merge pull request #5 from zhangvi7/github/serializers
zhangvi7 866e733
Merge pull request #7 from zhangvi7/github/datadoc-link
zhangvi7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from app.datasource import register | ||
from lib.github.github import github_manager | ||
from typing import Dict | ||
|
||
|
||
@register("/github/auth/", methods=["GET"]) | ||
def connect_github() -> Dict[str, str]: | ||
return github_manager.initiate_github_integration() | ||
|
||
|
||
@register("/github/is_authenticated/", methods=["GET"]) | ||
def is_github_authenticated() -> str: | ||
is_authenticated = github_manager.get_github_token() is not None | ||
return {"is_authenticated": is_authenticated} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import certifi | ||
from flask import session as flask_session, request | ||
from app.auth.github_auth import GitHubLoginManager | ||
from env import QuerybookSettings | ||
from lib.logger import get_logger | ||
from app.flask_app import flask_app | ||
from typing import Optional, Dict, Any | ||
|
||
LOG = get_logger(__file__) | ||
|
||
GITHUB_OAUTH_CALLBACK = "/github/oauth2callback" | ||
GITHUB_ACCESS_TOKEN = "github_access_token" | ||
|
||
|
||
class GitHubManager(GitHubLoginManager): | ||
def __init__( | ||
self, | ||
additional_scopes: Optional[list] = None, | ||
client_id: Optional[str] = None, | ||
client_secret: Optional[str] = None, | ||
): | ||
self.additional_scopes = additional_scopes or [] | ||
self._client_id = client_id | ||
self._client_secret = client_secret | ||
super().__init__() | ||
|
||
@property | ||
def oauth_config(self) -> Dict[str, Any]: | ||
config = super().oauth_config | ||
config["scope"] = "user email " + " ".join(self.additional_scopes) | ||
config[ | ||
"callback_url" | ||
] = f"{QuerybookSettings.PUBLIC_URL}{GITHUB_OAUTH_CALLBACK}" | ||
if self._client_id: | ||
config["client_id"] = self._client_id | ||
if self._client_secret: | ||
config["client_secret"] = self._client_secret | ||
return config | ||
|
||
def save_github_token(self, token: str) -> None: | ||
flask_session[GITHUB_ACCESS_TOKEN] = token | ||
LOG.debug("Saved GitHub token to session") | ||
|
||
def get_github_token(self) -> Optional[str]: | ||
return flask_session.get(GITHUB_ACCESS_TOKEN) | ||
|
||
def initiate_github_integration(self) -> Dict[str, str]: | ||
github = self.oauth_session | ||
authorization_url, state = github.authorization_url( | ||
self.oauth_config["authorization_url"] | ||
) | ||
flask_session["oauth_state"] = state | ||
return {"url": authorization_url} | ||
|
||
def github_integration_callback(self) -> str: | ||
try: | ||
github = self.oauth_session | ||
access_token = github.fetch_token( | ||
self.oauth_config["token_url"], | ||
client_secret=self.oauth_config["client_secret"], | ||
authorization_response=request.url, | ||
cert=certifi.where(), | ||
) | ||
self.save_github_token(access_token["access_token"]) | ||
return self.success_response() | ||
except Exception as e: | ||
LOG.error(f"Failed to obtain credentials: {e}") | ||
return self.error_response(str(e)) | ||
|
||
def success_response(self) -> str: | ||
return """ | ||
<p>Success! Please close the tab.</p> | ||
<script> | ||
window.opener.receiveChildMessage() | ||
</script> | ||
""" | ||
|
||
def error_response(self, error_message: str) -> str: | ||
return f""" | ||
<p>Failed to obtain credentials, reason: {error_message}</p> | ||
""" | ||
|
||
|
||
github_manager = GitHubManager( | ||
additional_scopes=["repo"], | ||
client_id=QuerybookSettings.GITHUB_CLIENT_ID, | ||
client_secret=QuerybookSettings.GITHUB_CLIENT_SECRET, | ||
) | ||
|
||
|
||
@flask_app.route(GITHUB_OAUTH_CALLBACK) | ||
def github_callback() -> str: | ||
return github_manager.github_integration_callback() |
61 changes: 61 additions & 0 deletions
61
querybook/webapp/components/DataDocGitHub/DataDocGitHubButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import React, { useCallback, useEffect, useState } from 'react'; | ||
|
||
import { GitHubResource } from 'resource/github'; | ||
import { IconButton } from 'ui/Button/IconButton'; | ||
|
||
import { GitHubModal } from './GitHubModal'; | ||
|
||
interface IProps { | ||
docId: number; | ||
} | ||
|
||
export const DataDocGitHubButton: React.FunctionComponent<IProps> = ({ | ||
docId, | ||
}) => { | ||
const [isModalOpen, setIsModalOpen] = useState(false); | ||
const [isAuthenticated, setIsAuthenticated] = useState(false); | ||
|
||
useEffect(() => { | ||
const checkAuthentication = async () => { | ||
try { | ||
const { data } = await GitHubResource.isAuthenticated(); | ||
setIsAuthenticated(data.is_authenticated); | ||
} catch (error) { | ||
console.error( | ||
'Failed to check GitHub authentication status:', | ||
error | ||
); | ||
} | ||
}; | ||
|
||
checkAuthentication(); | ||
}, []); | ||
|
||
const handleOpenModal = useCallback(() => { | ||
setIsModalOpen(true); | ||
}, []); | ||
|
||
const handleCloseModal = useCallback(() => { | ||
setIsModalOpen(false); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<IconButton | ||
icon="Github" | ||
onClick={handleOpenModal} | ||
tooltip="Connect to GitHub" | ||
tooltipPos="left" | ||
title="GitHub" | ||
/> | ||
{isModalOpen && ( | ||
<GitHubModal | ||
docId={docId} | ||
isAuthenticated={isAuthenticated} | ||
setIsAuthenticated={setIsAuthenticated} | ||
onClose={handleCloseModal} | ||
/> | ||
)} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.GitHubAuth { | ||
text-align: center; | ||
padding: 20px; | ||
} | ||
|
||
.GitHubAuth-icon { | ||
margin-bottom: 20px; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React from 'react'; | ||
|
||
import { Button } from 'ui/Button/Button'; | ||
import { Icon } from 'ui/Icon/Icon'; | ||
import { Message } from 'ui/Message/Message'; | ||
|
||
import './GitHub.scss'; | ||
|
||
interface IProps { | ||
onAuthenticate: () => void; | ||
} | ||
|
||
export const GitHubAuth: React.FunctionComponent<IProps> = ({ | ||
onAuthenticate, | ||
}) => ( | ||
<div className="GitHubAuth"> | ||
<Icon name="Github" size={64} className="GitHubAuth-icon" /> | ||
<Message | ||
title="Connect to GitHub" | ||
message="You currently do not have a GitHub provider linked to your account. Please authenticate to enable GitHub features on Querybook." | ||
type="info" | ||
iconSize={32} | ||
/> | ||
<Button | ||
onClick={onAuthenticate} | ||
title="Connect Now" | ||
color="accent" | ||
theme="fill" | ||
/> | ||
</div> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import React, { useCallback, useState } from 'react'; | ||
|
||
import { ComponentType, ElementType } from 'const/analytics'; | ||
import { trackClick } from 'lib/analytics'; | ||
import { GitHubResource, IGitHubAuthResponse } from 'resource/github'; | ||
import { Message } from 'ui/Message/Message'; | ||
import { Modal } from 'ui/Modal/Modal'; | ||
|
||
import { GitHubAuth } from './GitHubAuth'; | ||
|
||
interface IProps { | ||
docId: number; | ||
isAuthenticated: boolean; | ||
setIsAuthenticated: (isAuthenticated: boolean) => void; | ||
onClose: () => void; | ||
} | ||
|
||
export const GitHubModal: React.FunctionComponent<IProps> = ({ | ||
docId, | ||
isAuthenticated, | ||
setIsAuthenticated, | ||
onClose, | ||
}) => { | ||
const [errorMessage, setErrorMessage] = useState<string>(null); | ||
|
||
const handleConnectGitHub = useCallback(async () => { | ||
trackClick({ | ||
component: ComponentType.DATADOC_PAGE, | ||
element: ElementType.GITHUB_CONNECT_BUTTON, | ||
}); | ||
|
||
try { | ||
const { data }: { data: IGitHubAuthResponse } = | ||
await GitHubResource.connectGithub(); | ||
const url = data.url; | ||
if (!url) { | ||
throw new Error('Failed to get GitHub authentication URL'); | ||
} | ||
const authWindow = window.open(url); | ||
|
||
const receiveMessage = () => { | ||
authWindow.close(); | ||
delete window.receiveChildMessage; | ||
window.removeEventListener('message', receiveMessage, false); | ||
setIsAuthenticated(true); | ||
}; | ||
window.receiveChildMessage = receiveMessage; | ||
|
||
// If the user closes the authentication window manually, clean up | ||
const timer = setInterval(() => { | ||
if (authWindow.closed) { | ||
clearInterval(timer); | ||
window.removeEventListener( | ||
'message', | ||
receiveMessage, | ||
false | ||
); | ||
throw new Error('Authentication process failed'); | ||
} | ||
}, 1000); | ||
} catch (error) { | ||
console.error('GitHub authentication failed:', error); | ||
setErrorMessage('GitHub authentication failed. Please try again.'); | ||
} | ||
}, [setIsAuthenticated]); | ||
|
||
return ( | ||
<Modal onHide={onClose} title="GitHub Integration"> | ||
<div className="GitHubModal-content"> | ||
{isAuthenticated ? ( | ||
<Message message="Connected to GitHub!" type="success" /> | ||
) : ( | ||
<GitHubAuth onAuthenticate={handleConnectGitHub} /> | ||
)} | ||
{errorMessage && ( | ||
<Message message={errorMessage} type="error" /> | ||
)} | ||
<button onClick={onClose}>Close</button> | ||
</div> | ||
</Modal> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import ds from 'lib/datasource'; | ||
|
||
export interface IGitHubAuthResponse { | ||
url: string; | ||
} | ||
|
||
export const GitHubResource = { | ||
connectGithub: () => ds.fetch<IGitHubAuthResponse>('/github/auth/'), | ||
isAuthenticated: () => | ||
ds.fetch<{ is_authenticated: boolean }>('/github/is_authenticated/'), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
why it needs a timer here?
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.
detects if the user manually closed the authentication window to throw an error authentication process incomplete