-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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: use personal access token in OAuth2 databases #20280
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/** | ||
* Licensed to the Apache Software Foundation (ASF) under one | ||
* or more contributor license agreements. See the NOTICE file | ||
* distributed with this work for additional information | ||
* regarding copyright ownership. The ASF licenses this file | ||
* to you under the Apache License, Version 2.0 (the | ||
* "License"); you may not use this file except in compliance | ||
* with the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
import React from 'react'; | ||
import { t } from '@superset-ui/core'; | ||
|
||
import { ErrorMessageComponentProps } from './types'; | ||
import ErrorAlert from './ErrorAlert'; | ||
|
||
interface OAuth2RedirectExtra { | ||
url: string; | ||
} | ||
|
||
function OAuth2RedirectMessage({ | ||
error, | ||
source = 'sqllab', | ||
}: ErrorMessageComponentProps<OAuth2RedirectExtra>) { | ||
const { extra, level } = error; | ||
|
||
const body = ( | ||
<p> | ||
This database uses OAuth2 for authentication, and will store your personal | ||
access tokens after authentication so that only you can use it. When you | ||
click the link above you will be asked to grant access to the data in a | ||
new window. After confirming, you can close the window and re-run the | ||
query here. | ||
</p> | ||
); | ||
const subtitle = ( | ||
<> | ||
You need to{' '} | ||
<a href={extra.url} target="_blank" rel="noreferrer"> | ||
provide authorization | ||
</a>{' '} | ||
in order to run this query. | ||
</> | ||
); | ||
|
||
return ( | ||
<ErrorAlert | ||
title={t('Authorization needed')} | ||
subtitle={subtitle} | ||
level={level} | ||
source={source} | ||
body={body} | ||
/> | ||
); | ||
} | ||
|
||
export default OAuth2RedirectMessage; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,18 +17,27 @@ | |
# pylint: disable=too-many-lines | ||
import json | ||
import logging | ||
from datetime import datetime | ||
from datetime import datetime, timedelta | ||
from io import BytesIO | ||
from typing import Any, cast, Dict, List, Optional | ||
from zipfile import is_zipfile, ZipFile | ||
|
||
from flask import request, Response, send_file | ||
import jwt | ||
from flask import ( | ||
current_app, | ||
g, | ||
make_response, | ||
render_template, | ||
request, | ||
Response, | ||
send_file, | ||
) | ||
from flask_appbuilder.api import expose, protect, rison, safe | ||
from flask_appbuilder.models.sqla.interface import SQLAInterface | ||
from marshmallow import ValidationError | ||
from sqlalchemy.exc import NoSuchTableError, OperationalError, SQLAlchemyError | ||
|
||
from superset import app, event_logger | ||
from superset import app, db, event_logger | ||
from superset.commands.importers.exceptions import ( | ||
IncorrectFormatError, | ||
NoValidFilesFoundError, | ||
|
@@ -77,7 +86,7 @@ | |
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType | ||
from superset.exceptions import SupersetErrorsException, SupersetException | ||
from superset.extensions import security_manager | ||
from superset.models.core import Database | ||
from superset.models.core import Database, DatabaseUserOAuth2Tokens | ||
from superset.superset_typing import FlaskResponse | ||
from superset.utils.core import error_msg_from_exception, parse_js_uri_path_item | ||
from superset.views.base import json_errors_response | ||
|
@@ -107,6 +116,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): | |
"available", | ||
"validate_parameters", | ||
"validate_sql", | ||
"oauth2", | ||
} | ||
resource_name = "database" | ||
class_permission_name = "Database" | ||
|
@@ -855,6 +865,69 @@ def validate_sql(self, pk: int) -> FlaskResponse: | |
except DatabaseNotFoundError: | ||
return self.response_404() | ||
|
||
@expose("/oauth2/", methods=["GET"]) | ||
@event_logger.log_this_with_context( | ||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.oauth", | ||
log_to_statsd=True, | ||
) | ||
betodealmeida marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def oauth2(self) -> FlaskResponse: | ||
""" | ||
--- | ||
get: | ||
summary: >- | ||
Receive user-level authentication tokens from OAuth | ||
description: -> | ||
Receive and store user-level authentication tokens from OAuth | ||
parameters: | ||
- in: query | ||
name: state | ||
- in: query | ||
name: code | ||
- in: query | ||
name: scope | ||
- in: query | ||
name: error | ||
responses: | ||
200: | ||
description: A dummy self-closing HTML page | ||
content: | ||
text/html: | ||
schema: | ||
type: string | ||
400: | ||
$ref: '#/components/responses/400' | ||
500: | ||
$ref: '#/components/responses/500' | ||
""" | ||
parameters = request.args.to_dict() | ||
if "error" in parameters: | ||
raise Exception(parameters["error"]) # XXX: raise custom SIP-40 exception | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a todo? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normally I use it for temporary todos, while still working on the PR. |
||
|
||
payload = jwt.decode( | ||
parameters["state"].replace("%2E", "."), | ||
current_app.config["SECRET_KEY"], | ||
algorithms=["HS256"], | ||
) | ||
|
||
# exchange code for access/refresh tokens | ||
database = db.session.query(Database).filter_by(id=payload["database_id"]).one() | ||
token_response = database.db_engine_spec.get_oauth2_token(parameters["code"]) | ||
|
||
# store tokens | ||
token = DatabaseUserOAuth2Tokens( | ||
user_id=payload["user_id"], | ||
database_id=database.id, | ||
access_token=token_response["access_token"], | ||
access_token_expiration=datetime.now() | ||
+ timedelta(seconds=token_response["expires_in"]), | ||
refresh_token=token_response.get("refresh_token"), | ||
) | ||
db.session.add(token) | ||
db.session.commit() | ||
|
||
# return blank page that closes itself | ||
return make_response(render_template("superset/close.html"), 200) | ||
|
||
@expose("/export/", methods=["GET"]) | ||
@protect() | ||
@safe | ||
|
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.
Maybe this should be a dict: