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

feat(ssh-tunnelling): Setup SSH Tunneling Commands for Database Connections #21912

Merged
merged 98 commits into from
Jan 3, 2023
Merged
Show file tree
Hide file tree
Changes from 86 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
830a283
save
hughhhh Oct 19, 2022
2c1e736
create migration
hughhhh Oct 20, 2022
f78df83
created schema and rename
hughhhh Oct 21, 2022
d482df4
linting
hughhhh Oct 21, 2022
9edb581
fix encrpytions
hughhhh Oct 21, 2022
da27d8f
remove map tabl
hughhhh Oct 24, 2022
773a6c8
fix linting
hughhhh Oct 25, 2022
2f2dda2
add constraint
hughhhh Oct 25, 2022
fd0d7f2
add fk to migration
hughhhh Oct 25, 2022
158da8d
init
hughhhh Oct 26, 2022
face73f
update all the examples
hughhhh Oct 26, 2022
95d079e
change remaining bits
hughhhh Oct 26, 2022
d5926e3
add id
hughhhh Oct 27, 2022
f7a6a41
use factory instead
hughhhh Oct 28, 2022
30e380a
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Oct 28, 2022
87c0d79
Merge branch 'master' into ref-get-sqla-engine-2
hughhhh Oct 28, 2022
11b240b
fix confict
hughhhh Oct 28, 2022
1bfdbda
fix conflict
hughhhh Oct 28, 2022
4146d5a
setup return value for contextmanager
hughhhh Oct 31, 2022
f8b877d
add sshtunnel pip
hughhhh Oct 31, 2022
54fc147
updates test
hughhhh Nov 1, 2022
fdc6ca3
fix linting
hughhhh Nov 2, 2022
66c0801
renaming function
hughhhh Nov 3, 2022
1f9ec5e
fix test
hughhhh Nov 5, 2022
c698cf4
Merge branch 'ref-get-sqla-engine-2' of https://github.com/apache/sup…
hughhhh Nov 7, 2022
41bd19b
add schema to test_connection api
hughhhh Nov 7, 2022
8811a99
fix get engine to return contextmanager
hughhhh Nov 7, 2022
82d7532
why
hughhhh Nov 7, 2022
1f829ac
yerp
hughhhh Nov 7, 2022
d53d116
update typing
hughhhh Nov 7, 2022
752161d
update comment
hughhhh Nov 7, 2022
8c4b081
Merge branch 'ref-get-sqla-engine-2' of https://github.com/apache/sup…
hughhhh Nov 7, 2022
1a19a97
save
hughhhh Nov 8, 2022
58b9cce
save
hughhhh Nov 8, 2022
0ac6fb1
Merge branch 'master' of https://github.com/apache/superset into ref-…
hughhhh Nov 8, 2022
31f3c1d
fix pylint
hughhhh Nov 8, 2022
a0b30e6
Merge branch 'ref-get-sqla-engine-2' of https://github.com/apache/sup…
hughhhh Nov 8, 2022
e089a8d
last one
hughhhh Nov 8, 2022
81b2f88
Merge branch 'ref-get-sqla-engine-2' of https://github.com/apache/sup…
hughhhh Nov 8, 2022
45686b7
update naming on ssh tunnel
hughhhh Nov 9, 2022
7ce5836
Merge branch 'master' into ref-get-sqla-engine-2
hughhhh Nov 9, 2022
d9c8d0d
Merge branch 'ref-get-sqla-engine-2' of https://github.com/apache/sup…
hughhhh Nov 9, 2022
ec27b80
fix renaming
hughhhh Nov 10, 2022
65e3e29
fix renaming 2
hughhhh Nov 10, 2022
9fa9db5
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Nov 10, 2022
1a11ff4
oops
hughhhh Nov 10, 2022
3f0dae1
fix linting errors
hughhhh Nov 10, 2022
2777807
feat(ssh_tunnel): DAO Changes for SSH Tunnel (#22120)
Antonio-RiveroMartnez Nov 15, 2022
8ed02cd
fix merge conflicts
hughhhh Nov 16, 2022
6a68147
Merge branch 'create-sshtunnelconfig-tbl' of https://github.com/apach…
hughhhh Nov 16, 2022
6bd32e8
feat(ssh_tunnel): Delete command & exceptions (#22131)
Antonio-RiveroMartnez Nov 16, 2022
8a3ee35
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Nov 16, 2022
bc89194
Merge branch 'create-sshtunnelconfig-tbl' of https://github.com/apach…
hughhhh Nov 16, 2022
adb9451
fix indenting for superset/databases/commands/validate.py
hughhhh Nov 17, 2022
16d960b
change tablename
hughhhh Nov 17, 2022
d2ab4a6
feat(ssh_tunnel): DELETE SSH Tunnels API (#22153)
Antonio-RiveroMartnez Nov 17, 2022
fb2acd0
Revert "feat(ssh_tunnel): DELETE SSH Tunnels API" (#22156)
hughhhh Nov 17, 2022
4d807c9
feat(ssh_tunnel): Update command & exceptions (#22132)
Antonio-RiveroMartnez Nov 17, 2022
dc0c848
forgot server_port
hughhhh Nov 17, 2022
21fcdf0
bind_port + bind_host :)
hughhhh Nov 17, 2022
68cb75f
oops
hughhhh Nov 17, 2022
44ca56b
fix linting
hughhhh Nov 17, 2022
7e1461e
feat(ssh_tunnel): SSH Tunnel updates from Code Review (#22182)
Antonio-RiveroMartnez Nov 21, 2022
92e41f1
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Nov 22, 2022
6c59663
feat(ssh_tunnel): Create command & exceptions (#22148)
hughhhh Nov 22, 2022
466703a
Update schemas.py
hughhhh Nov 28, 2022
554de53
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Nov 30, 2022
4448739
chore(ssh-tunnel): create `contextmanager` for sql.inspect (#22251)
hughhhh Nov 30, 2022
bb78055
fix lint
hughhhh Nov 30, 2022
f507385
fix migrations
hughhhh Nov 30, 2022
45aa022
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Dec 1, 2022
3d3b71b
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Dec 1, 2022
86436b6
Revert "chore(ssh-tunnel): create `contextmanager` for sql.inspect (#…
hughhhh Dec 1, 2022
54a8d7f
debugging
hughhhh Dec 1, 2022
3f6afec
fix(ssh_tunnel): Address Base PR comments from peer review (#22306)
Antonio-RiveroMartnez Dec 5, 2022
7625566
fix pre-commit
hughhhh Dec 5, 2022
0578a8e
working changes
hughhhh Dec 6, 2022
ec20429
refactor bind_host and bind_port
hughhhh Dec 6, 2022
1f57d4a
refactor create flow for temp ssh tunnels
hughhhh Dec 7, 2022
ed19a3e
remove logger
hughhhh Dec 8, 2022
852c8bb
chore(ssh_tunnel): Add extra tests to SSHTunnel commands (#22372)
Antonio-RiveroMartnez Dec 8, 2022
be5c005
add flush to allow database.id to be populated
hughhhh Dec 8, 2022
948f748
Merge branch 'create-sshtunnelconfig-tbl' of https://github.com/apach…
hughhhh Dec 8, 2022
c636ce7
make sure to use inspector with context
hughhhh Dec 9, 2022
908896f
remove id and database_id
hughhhh Dec 12, 2022
e3ef835
uselist
hughhhh Dec 12, 2022
c5c50ed
feat(ssh-tunnel): ssh manager config + feature flag (#22201)
hughhhh Dec 15, 2022
06e115b
update kwarg function name
hughhhh Dec 15, 2022
13ed50d
chore(ssh-tunnel): Move SSHManager to extensions pattern (#22433)
hughhhh Dec 16, 2022
54d51e2
add flag to indicate ssh tunneling is enabled for this engine
hughhhh Dec 16, 2022
53eaa63
Update superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_cre…
hughhhh Dec 19, 2022
8f8faff
Update superset/migrations/versions/2022-10-20_10-48_f3c2d8ec8595_cre…
hughhhh Dec 19, 2022
607c682
fix linting
hughhhh Dec 20, 2022
1ea0e8b
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Dec 22, 2022
7cc7bc8
fix requirements
hughhhh Dec 22, 2022
7c539d2
Merge branch 'master' of https://github.com/apache/superset into crea…
hughhhh Jan 3, 2023
394afc1
get df with get_raw_connection function
hughhhh Jan 3, 2023
9b09fc7
feat(ssh_tunnel): APIs for SSH Tunnels (#22199)
Antonio-RiveroMartnez Jan 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ babel==2.9.1
# via flask-babel
backoff==1.11.1
# via apache-superset
bcrypt==4.0.1
# via paramiko
billiard==3.6.4.0
# via celery
bleach==3.3.1
Expand All @@ -30,7 +32,9 @@ cachelib==0.4.1
celery==5.2.2
# via apache-superset
cffi==1.14.6
# via cryptography
# via
# cryptography
# pynacl
click==8.0.4
# via
# apache-superset
Expand All @@ -57,7 +61,9 @@ cron-descriptor==1.2.24
croniter==1.0.15
# via apache-superset
cryptography==3.4.7
# via apache-superset
# via
# apache-superset
# paramiko
deprecation==2.1.0
# via apache-superset
dnspython==2.1.0
Expand Down Expand Up @@ -167,6 +173,8 @@ packaging==21.3
# deprecation
pandas==1.4.4
# via apache-superset
paramiko==2.11.0
# via sshtunnel
parsedatetime==2.6
# via apache-superset
pgsanity==0.2.9
Expand All @@ -188,6 +196,8 @@ pyjwt==2.4.0
# flask-jwt-extended
pymeeus==0.5.11
# via convertdate
pynacl==1.5.0
# via paramiko
pyparsing==3.0.6
# via
# apache-superset
Expand Down Expand Up @@ -231,6 +241,7 @@ six==1.16.0
# flask-talisman
# isodate
# jsonschema
# paramiko
# polyline
# prison
# pyrsistent
Expand All @@ -252,6 +263,8 @@ sqlalchemy-utils==0.38.3
# flask-appbuilder
sqlparse==0.4.3
# via apache-superset
sshtunnel==0.4.0
# via apache-superset
tabulate==0.8.9
# via apache-superset
typing-extensions==4.4.0
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def get_git_sha() -> str:
"PyJWT>=2.4.0, <3.0",
"redis",
"selenium>=3.141.0",
"sshtunnel>=0.4.0, <0.5",
"simplejson>=3.15.0",
"slack_sdk>=3.1.1, <4",
"sqlalchemy>=1.4, <2",
Expand Down
2 changes: 2 additions & 0 deletions superset/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

NO_TIME_RANGE = "No filter"

SSH_TUNNELLING_LOCAL_BIND_ADDRESS = "127.0.0.1"


class RouteMethod: # pylint: disable=too-few-public-methods
"""
Expand Down
15 changes: 14 additions & 1 deletion superset/databases/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
)
from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
from superset.databases.dao import DatabaseDAO
from superset.databases.ssh_tunnel.dao import SSHTunnelDAO
from superset.exceptions import SupersetErrorsException
from superset.extensions import db, event_logger, security_manager

Expand Down Expand Up @@ -70,13 +71,25 @@ def run(self) -> Model:
try:
database = DatabaseDAO.create(self._properties, commit=False)
database.set_sqlalchemy_uri(database.sqlalchemy_uri)
db.session.flush()

ssh_tunnel = None
eschutho marked this conversation as resolved.
Show resolved Hide resolved
if ssh_tunnel_properties := self._properties.get("ssh_tunnel"):
ssh_tunnel = SSHTunnelDAO.create(
{
**ssh_tunnel_properties,
"database_id": database.id,
},
commit=False,
eschutho marked this conversation as resolved.
Show resolved Hide resolved
)

# adding a new database we always want to force refresh schema list
schemas = database.get_all_schema_names(cache=False)
schemas = database.get_all_schema_names(cache=False, ssh_tunnel=ssh_tunnel)
for schema in schemas:
security_manager.add_permission_view_menu(
"schema_access", security_manager.get_schema_perm(database, schema)
)

db.session.commit()
except DAOCreateFailedError as ex:
db.session.rollback()
Expand Down
9 changes: 8 additions & 1 deletion superset/databases/commands/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
DatabaseTestConnectionUnexpectedError,
)
from superset.databases.dao import DatabaseDAO
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.databases.utils import make_url_safe
from superset.errors import ErrorLevel, SupersetErrorType
from superset.exceptions import (
Expand Down Expand Up @@ -90,6 +91,10 @@ def run(self) -> None: # pylint: disable=too-many-statements
database.set_sqlalchemy_uri(uri)
database.db_engine_spec.mutate_db_for_connection_test(database)

# Generate tunnel if present in the properties
if ssh_tunnel := self._properties.get("ssh_tunnel"):
ssh_tunnel = SSHTunnel(**ssh_tunnel)

event_logger.log_with_context(
action="test_connection_attempt",
engine=database.db_engine_spec.__name__,
Expand All @@ -99,7 +104,9 @@ def ping(engine: Engine) -> bool:
with closing(engine.raw_connection()) as conn:
return engine.dialect.do_ping(conn)

with database.get_sqla_engine_with_context() as engine:
with database.get_sqla_engine_with_context(
override_ssh_tunnel=ssh_tunnel
) as engine:
try:
alive = func_timeout(
app.config["TEST_DATABASE_CONNECTION_TIMEOUT"].total_seconds(),
Expand Down
11 changes: 11 additions & 0 deletions superset/databases/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from superset.dao.base import BaseDAO
from superset.databases.filters import DatabaseFilter
from superset.databases.ssh_tunnel.models import SSHTunnel
from superset.extensions import db
from superset.models.core import Database
from superset.models.dashboard import Dashboard
Expand Down Expand Up @@ -124,3 +125,13 @@ def get_related_objects(cls, database_id: int) -> Dict[str, Any]:
return dict(
charts=charts, dashboards=dashboards, sqllab_tab_states=sqllab_tab_states
)

@classmethod
def get_ssh_tunnel(cls, database_id: int) -> Optional[SSHTunnel]:
ssh_tunnel = (
db.session.query(SSHTunnel)
.filter(SSHTunnel.database_id == database_id)
.one_or_none()
)

return ssh_tunnel
16 changes: 16 additions & 0 deletions superset/databases/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,19 @@ class Meta: # pylint: disable=too-few-public-methods
)


class DatabaseSSHTunnel(Schema):
server_address = fields.String()
server_port = fields.Integer()
username = fields.String()

# Basic Authentication
password = fields.String(required=False)

# password protected private key authentication
private_key = fields.String(required=False)
private_key_password = fields.String(required=False)


class DatabasePostSchema(Schema, DatabaseParametersSchemaMixin):
class Meta: # pylint: disable=too-few-public-methods
unknown = EXCLUDE
Expand Down Expand Up @@ -409,6 +422,7 @@ class Meta: # pylint: disable=too-few-public-methods
is_managed_externally = fields.Boolean(allow_none=True, default=False)
external_url = fields.String(allow_none=True)
uuid = fields.String(required=False)
ssh_tunnel = fields.Nested(DatabaseSSHTunnel, allow_none=True)
Copy link
Member

Choose a reason for hiding this comment

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

do we need this on DatabasePutSchema also?
see if creating a DatabaseSSHTunnelPostSchema and DatabaseSSHTunnelPutSchema is needed



class DatabasePutSchema(Schema, DatabaseParametersSchemaMixin):
Expand Down Expand Up @@ -482,6 +496,8 @@ class DatabaseTestConnectionSchema(Schema, DatabaseParametersSchemaMixin):
validate=[Length(1, 1024), sqlalchemy_uri_validator],
)

ssh_tunnel = fields.Nested(DatabaseSSHTunnel, allow_none=True)


class TableMetadataOptionsResponseSchema(Schema):
deferrable = fields.Bool()
Expand Down
16 changes: 16 additions & 0 deletions superset/databases/ssh_tunnel/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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.
16 changes: 16 additions & 0 deletions superset/databases/ssh_tunnel/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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.
82 changes: 82 additions & 0 deletions superset/databases/ssh_tunnel/commands/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# 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 logging
from typing import Any, Dict, List, Optional

from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError

from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAOCreateFailedError
from superset.databases.ssh_tunnel.commands.exceptions import (
SSHTunnelCreateFailedError,
SSHTunnelInvalidError,
SSHTunnelRequiredFieldValidationError,
)
from superset.databases.ssh_tunnel.dao import SSHTunnelDAO
from superset.extensions import event_logger

logger = logging.getLogger(__name__)


class CreateSSHTunnelCommand(BaseCommand):
def __init__(self, database_id: int, data: Dict[str, Any]):
self._properties = data.copy()
self._properties["database_id"] = database_id

def run(self) -> Model:
self.validate()

try:
tunnel = SSHTunnelDAO.create(self._properties, commit=False)
except DAOCreateFailedError as ex:
raise SSHTunnelCreateFailedError() from ex

return tunnel

def validate(self) -> None:
# TODO(hughhh): check to make sure the server port is not localhost
# using the config.SSH_TUNNEL_MANAGER
Comment on lines +62 to +63
Copy link
Member

Choose a reason for hiding this comment

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

If the command is called when the tunnel is added to the DB then we don't want to check the localhost here; we want to check every time the connection is made, since the value of the config might change after the ssh tunnel has been created.

exceptions: List[ValidationError] = []
database_id: Optional[int] = self._properties.get("database_id")
server_address: Optional[str] = self._properties.get("server_address")
server_port: Optional[int] = self._properties.get("server_port")
username: Optional[str] = self._properties.get("username")
private_key: Optional[str] = self._properties.get("private_key")
private_key_password: Optional[str] = self._properties.get(
"private_key_password"
)
if not database_id:
exceptions.append(SSHTunnelRequiredFieldValidationError("database_id"))
if not server_address:
exceptions.append(SSHTunnelRequiredFieldValidationError("server_address"))
if not server_port:
exceptions.append(SSHTunnelRequiredFieldValidationError("server_port"))
if not username:
exceptions.append(SSHTunnelRequiredFieldValidationError("username"))
if private_key_password and private_key is None:
exceptions.append(SSHTunnelRequiredFieldValidationError("private_key"))
if exceptions:
exception = SSHTunnelInvalidError()
exception.add_list(exceptions)
event_logger.log_with_context(
action="ssh_tunnel_creation_failed.{}.{}".format(
exception.__class__.__name__,
".".join(exception.get_list_classnames()),
)
)
raise exception
51 changes: 51 additions & 0 deletions superset/databases/ssh_tunnel/commands/delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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 logging
from typing import Optional

from flask_appbuilder.models.sqla import Model

from superset.commands.base import BaseCommand
from superset.dao.exceptions import DAODeleteFailedError
from superset.databases.ssh_tunnel.commands.exceptions import (
SSHTunnelDeleteFailedError,
SSHTunnelNotFoundError,
)
from superset.databases.ssh_tunnel.dao import SSHTunnelDAO
from superset.databases.ssh_tunnel.models import SSHTunnel

logger = logging.getLogger(__name__)


class DeleteSSHTunnelCommand(BaseCommand):
def __init__(self, model_id: int):
self._model_id = model_id
self._model: Optional[SSHTunnel] = None

def run(self) -> Model:
self.validate()
try:
ssh_tunnel = SSHTunnelDAO.delete(self._model)
except DAODeleteFailedError as ex:
raise SSHTunnelDeleteFailedError() from ex
return ssh_tunnel

def validate(self) -> None:
# Validate/populate model exists
self._model = SSHTunnelDAO.find_by_id(self._model_id)
if not self._model:
raise SSHTunnelNotFoundError()
Loading