Skip to content

Commit da50323

Browse files
committed
Add SQLAlchemy implementation to OAuth stores
Also, this pull request updates the structure of unit tests/integration tests
1 parent e0627b6 commit da50323

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+339
-36
lines changed

integration_tests/helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def is_not_specified() -> bool:
2121
module = inspect.getmodule(frame[0])
2222
filepath: str = module.__file__
2323

24-
# python setup.py run_all_tests --integration-test-target=web/test_issue_560.py
25-
test_target: str = sys.argv[3] # e.g., web/test_issue_560.py
24+
# python setup.py integration_tests --test-target=web/test_issue_560.py
25+
test_target: str = sys.argv[1] # e.g., web/test_issue_560.py
2626
return not test_target or \
2727
not filepath.endswith(test_target)

setup.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"flake8>=3,<4",
2525
"black==19.10b0",
2626
"psutil>=5,<6",
27+
"databases>=0.3",
2728
]
2829
codegen_dependencies = [
2930
"black==19.10b0",
@@ -182,7 +183,9 @@ def run(self):
182183
[sys.executable, "-m", "pip", "install"] + validate_dependencies,
183184
)
184185
self._run("Running black ...", [sys.executable, "-m", "black", f"{here}/slack"])
185-
self._run("Running black ...", [sys.executable, "-m", "black", f"{here}/slack_sdk"])
186+
self._run(
187+
"Running black ...", [sys.executable, "-m", "black", f"{here}/slack_sdk"]
188+
)
186189
self._run(
187190
"Running flake8 ...", [sys.executable, "-m", "flake8", f"{here}/slack"]
188191
)
@@ -204,14 +207,30 @@ def run(self):
204207
)
205208

206209

210+
class UnitTestsCommand(BaseCommand):
211+
"""Support setup.py validate."""
212+
213+
description = "Run unit tests (pytest)."
214+
user_options = [("test-target=", "i", "tests/{test-target}")]
215+
216+
def initialize_options(self):
217+
self.test_target = ""
218+
219+
def run(self):
220+
target = self.test_target.replace("tests/", "", 1)
221+
self._run(
222+
"Running unit tests ...",
223+
[sys.executable, "-m", "pytest", f"tests/{target}",],
224+
)
225+
226+
207227
class IntegrationTestsCommand(BaseCommand):
208228
"""Support setup.py run_integration_tests"""
209229

210230
description = "Run integration tests (pytest)."
211231

212232
user_options = [
213233
("test-target=", "i", "integration_tests/{test-target}"),
214-
("legacy=", "i", "1"),
215234
]
216235

217236
def initialize_options(self):
@@ -220,19 +239,9 @@ def initialize_options(self):
220239

221240
def run(self):
222241
target = self.test_target.replace("integration_tests/", "", 1)
223-
path = (
224-
f"integration_tests_legacy/{target}"
225-
if self.legacy
226-
else f"integration_tests/{target}"
227-
)
242+
path = f"integration_tests/{target}"
228243
self._run(
229-
"Running integration tests ...",
230-
[
231-
sys.executable,
232-
"-m",
233-
"pytest",
234-
path,
235-
],
244+
"Running integration tests ...", [sys.executable, "-m", "pytest", path,],
236245
)
237246

238247

@@ -286,6 +295,8 @@ def run(self):
286295
"aiohttp>=3,<4",
287296
# used only under slack_sdk/*_store
288297
"boto3<=2",
298+
# InstallationStore/OAuthStateStore
299+
"SQLAlchemy>=1,<2",
289300
],
290301
},
291302
setup_requires=pytest_runner,
@@ -295,6 +306,7 @@ def run(self):
295306
"upload": UploadCommand,
296307
"codegen": CodegenCommand,
297308
"validate": ValidateCommand,
309+
"unit_tests": UnitTestsCommand,
298310
"integration_tests": IntegrationTestsCommand,
299311
},
300312
)

slack_sdk/oauth/installation_store/models/bot.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Optional, Union, List
1+
from datetime import datetime
2+
from typing import Optional, Union, List, Dict, Any
23

34

45
class Bot:
@@ -29,3 +30,15 @@ def __init__(
2930
else:
3031
self.bot_scopes = bot_scopes
3132
self.installed_at = installed_at
33+
34+
def to_dict(self) -> Dict[str, Any]:
35+
return {
36+
"app_id": self.app_id,
37+
"enterprise_id": self.enterprise_id,
38+
"team_id": self.team_id,
39+
"bot_token": self.bot_token,
40+
"bot_id": self.bot_id,
41+
"bot_user_id": self.bot_user_id,
42+
"bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
43+
"installed_at": datetime.utcfromtimestamp(self.installed_at),
44+
}

slack_sdk/oauth/installation_store/models/installation.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from datetime import datetime
12
from time import time
2-
from typing import Optional, List, Union
3+
from typing import Optional, List, Union, Dict, Any
34

45
from slack_sdk.oauth.installation_store.models.bot import Bot
56

@@ -64,3 +65,21 @@ def to_bot(self) -> Bot:
6465
bot_scopes=self.bot_scopes,
6566
installed_at=self.installed_at,
6667
)
68+
69+
def to_dict(self) -> Dict[str, Any]:
70+
return {
71+
"app_id": self.app_id,
72+
"enterprise_id": self.enterprise_id,
73+
"team_id": self.team_id,
74+
"bot_token": self.bot_token,
75+
"bot_id": self.bot_id,
76+
"bot_user_id": self.bot_user_id,
77+
"bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
78+
"user_id": self.user_id,
79+
"user_token": self.user_token,
80+
"user_scopes": ",".join(self.user_scopes) if self.user_scopes else None,
81+
"incoming_webhook_url": self.incoming_webhook_url,
82+
"incoming_webhook_channel_id": self.incoming_webhook_channel_id,
83+
"incoming_webhook_configuration_url": self.incoming_webhook_configuration_url,
84+
"installed_at": datetime.utcfromtimestamp(self.installed_at),
85+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import logging
2+
from logging import Logger
3+
from typing import Optional
4+
5+
from sqlalchemy.engine import Engine
6+
from slack_sdk.oauth.installation_store.installation_store import InstallationStore
7+
from slack_sdk.oauth.installation_store.models.bot import Bot
8+
from slack_sdk.oauth.installation_store.models.installation import Installation
9+
10+
import sqlalchemy
11+
from sqlalchemy import Table, Column, Integer, String, DateTime, Index, and_, desc
12+
13+
14+
class SQLAlchemyInstallationStore(InstallationStore):
15+
engine: Engine
16+
client_id: str
17+
18+
metadata = sqlalchemy.MetaData()
19+
installations: Table = sqlalchemy.Table(
20+
"installations",
21+
metadata,
22+
Column("id", Integer, primary_key=True, autoincrement=True),
23+
Column("client_id", String, nullable=False),
24+
Column("app_id", String, nullable=False),
25+
Column("enterprise_id", String),
26+
Column("team_id", String),
27+
Column("bot_token", String),
28+
Column("bot_id", String),
29+
Column("bot_user_id", String),
30+
Column("bot_scopes", String),
31+
Column("user_id", String, nullable=False),
32+
Column("user_token", String),
33+
Column("user_scopes", String),
34+
Column("incoming_webhook_url", String),
35+
Column("incoming_webhook_channel_id", String),
36+
Column("incoming_webhook_configuration_url", String),
37+
Column(
38+
"installed_at", DateTime, nullable=False, default=sqlalchemy.sql.func.now()
39+
),
40+
Index("installations_idx", "client_id", "enterprise_id", "team_id", "user_id",),
41+
)
42+
43+
bots = Table(
44+
"bots",
45+
metadata,
46+
Column("id", Integer, primary_key=True, autoincrement=True),
47+
Column("client_id", String, nullable=False),
48+
Column("app_id", String, nullable=False),
49+
Column("enterprise_id", String),
50+
Column("team_id", String),
51+
Column("bot_token", String),
52+
Column("bot_id", String),
53+
Column("bot_user_id", String),
54+
Column("bot_scopes", String),
55+
Column(
56+
"installed_at", DateTime, nullable=False, default=sqlalchemy.sql.func.now()
57+
),
58+
Index("bots_idx", "client_id", "enterprise_id", "team_id",),
59+
)
60+
61+
def __init__(
62+
self,
63+
client_id: str,
64+
engine: Engine,
65+
logger: Logger = logging.getLogger(__name__),
66+
):
67+
self.client_id = client_id
68+
self._logger = logger
69+
self.engine = engine
70+
71+
def create_tables(self):
72+
self.metadata.create_all(self.engine)
73+
74+
@property
75+
def logger(self) -> Logger:
76+
return self._logger
77+
78+
def save(self, installation: Installation):
79+
with self.engine.begin() as conn:
80+
i = installation.to_dict()
81+
i["client_id"] = self.client_id
82+
conn.execute(self.installations.insert(), i)
83+
b = installation.to_bot().to_dict()
84+
b["client_id"] = self.client_id
85+
conn.execute(self.bots.insert(), b)
86+
87+
def find_bot(
88+
self, *, enterprise_id: Optional[str], team_id: Optional[str]
89+
) -> Optional[Bot]:
90+
c = self.bots.c
91+
query = (
92+
self.bots.select()
93+
.where(and_(c.enterprise_id == enterprise_id, c.team_id == team_id))
94+
.order_by(desc(c.installed_at))
95+
.limit(1)
96+
)
97+
98+
with self.engine.connect() as conn:
99+
result: object = conn.execute(query)
100+
for row in result:
101+
return Bot(
102+
app_id=row["app_id"],
103+
enterprise_id=row["enterprise_id"],
104+
team_id=row["team_id"],
105+
bot_token=row["bot_token"],
106+
bot_id=row["bot_id"],
107+
bot_user_id=row["bot_user_id"],
108+
bot_scopes=row["bot_scopes"],
109+
installed_at=row["installed_at"],
110+
)
111+
return None

slack_sdk/oauth/installation_store/sqlite3/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ def create_tables(self):
6262
bot_id text not null,
6363
bot_user_id text not null,
6464
bot_scopes text,
65-
installer_user_id text not null,
66-
installer_user_token text,
67-
installer_user_scopes text,
65+
user_id text not null,
66+
user_token text,
67+
user_scopes text,
6868
incoming_webhook_url text,
6969
incoming_webhook_channel_id text,
7070
incoming_webhook_configuration_url text,
@@ -74,7 +74,7 @@ def create_tables(self):
7474
)
7575
conn.execute(
7676
"""
77-
create index installations_idx on installations (client_id, enterprise_id, team_id, installer_user_id);
77+
create index installations_idx on installations (client_id, enterprise_id, team_id, user_id);
7878
"""
7979
)
8080
conn.execute(
@@ -152,9 +152,9 @@ def save(self, installation: Installation):
152152
bot_id,
153153
bot_user_id,
154154
bot_scopes,
155-
installer_user_id,
156-
installer_user_token,
157-
installer_user_scopes,
155+
user_id,
156+
user_token,
157+
user_scopes,
158158
incoming_webhook_url,
159159
incoming_webhook_channel_id,
160160
incoming_webhook_configuration_url
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import logging
2+
import time
3+
from datetime import datetime
4+
from logging import Logger
5+
from uuid import uuid4
6+
7+
from ..state_store import OAuthStateStore
8+
import sqlalchemy
9+
from sqlalchemy import Table, Column, Integer, String, DateTime, and_
10+
from sqlalchemy.engine import Engine
11+
12+
13+
class SQLAlchemyOAuthStateStore(OAuthStateStore):
14+
engine: Engine
15+
expiration_seconds: int
16+
17+
metadata = sqlalchemy.MetaData()
18+
oauth_states: Table = sqlalchemy.Table(
19+
"oauth_states",
20+
metadata,
21+
Column("id", Integer, primary_key=True, autoincrement=True),
22+
Column("state", String, nullable=False),
23+
Column("expire_at", DateTime, nullable=False),
24+
)
25+
26+
def __init__(
27+
self,
28+
expiration_seconds: int,
29+
engine: Engine,
30+
logger: Logger = logging.getLogger(__name__),
31+
):
32+
self.expiration_seconds = expiration_seconds
33+
self._logger = logger
34+
self.engine = engine
35+
36+
@property
37+
def logger(self) -> Logger:
38+
if self._logger is None:
39+
self._logger = logging.getLogger(__name__)
40+
return self._logger
41+
42+
def issue(self) -> str:
43+
state: str = str(uuid4())
44+
now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
45+
with self.engine.begin() as conn:
46+
conn.execute(
47+
self.oauth_states.insert(), {"state": state, "expire_at": now},
48+
)
49+
return state
50+
51+
def consume(self, state: str) -> bool:
52+
try:
53+
with self.engine.begin() as conn:
54+
c = self.oauth_states.c
55+
query = self.oauth_states.select().where(
56+
and_(c.state == state, c.expire_at > datetime.utcnow())
57+
)
58+
result = conn.execute(query)
59+
for row in result:
60+
self.logger.debug(f"consume's query result: {row}")
61+
conn.execute(self.oauth_states.delete().where(c.id == row["id"]))
62+
return True
63+
return False
64+
except Exception as e: # skipcq: PYL-W0703
65+
message = f"Failed to find any persistent data for state: {state} - {e}"
66+
self.logger.warning(message)
67+
return False

tests/slack_sdk_tests/oauth/authorize_url_generator/__init__.py renamed to tests/slack_sdk/oauth/authorize_url_generator/__init__.py

File renamed without changes.

0 commit comments

Comments
 (0)