Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.9"
python-version: "3.12"

- name: pip install
run: |
Expand Down
93 changes: 64 additions & 29 deletions seatable_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,25 +89,20 @@ def _get_msg_sender_by_account(self, account_name):
msg_sender = None
return msg_sender

def auth(self, with_socket_io=False):
"""Auth to SeaTable
"""
def _auth(self, workspace_id, dtable_name, dtable_uuid, access_token, dtable_server_url, dtable_db_url, use_api_gateway, with_socket_io=False):
self.jwt_exp = datetime.now() + timedelta(days=3)
url = self.server_url + '/api/v2.1/dtable/app-access-token/'
headers = parse_headers(self.token)
response = requests.get(url, headers=headers, timeout=self.timeout)
data = parse_response(response)

self.dtable_server_url = parse_server_url(data.get('dtable_server'))
self.dtable_db_url = parse_server_url(data.get('dtable_db', ''))
self.jwt_token = data.get('access_token')
self.dtable_server_url = parse_server_url(dtable_server_url)
self.dtable_db_url = parse_server_url(dtable_db_url)

self.jwt_token = access_token
self.headers = parse_headers(self.jwt_token)
self.workspace_id = data.get('workspace_id')
self.dtable_uuid = data.get('dtable_uuid')
self.dtable_name = data.get('dtable_name')

# api gateway entry
self.use_api_gateway = data.get('use_api_gateway')
self.workspace_id = workspace_id
self.dtable_name = dtable_name
self.dtable_uuid = dtable_uuid

self.use_api_gateway = use_api_gateway
if self.use_api_gateway:
self.api_gateway = APIGateway(
token = self.token,
Expand All @@ -116,13 +111,34 @@ def auth(self, with_socket_io=False):
headers=self.headers,
dtable_uuid=self.dtable_uuid
)
self.dtable_server_url = parse_server_url(self.server_url + '/api-gateway')

self.is_authed = True

if with_socket_io is True:
base = self._clone()
self.socketIO = SocketIO(base)
self.socketIO._connect()

self.is_authed = True
def auth(self, with_socket_io=False):
"""Auth to SeaTable
"""
self.jwt_exp = datetime.now() + timedelta(days=3)
url = self.server_url + '/api/v2.1/dtable/app-access-token/'
headers = parse_headers(self.token)
response = requests.get(url, headers=headers, timeout=self.timeout)
data = parse_response(response)

self._auth(
data.get('workspace_id'),
data.get('dtable_name'),
data.get('dtable_uuid'),
data.get('access_token'),
data.get('dtable_server', ''),
data.get('dtable_db', ''),
data.get('use_api_gateway'),
with_socket_io=with_socket_io
)

def _metadata_server_url(self):
return self.dtable_server_url + '/api/v1/dtables/' + self.dtable_uuid + '/metadata/'
Expand Down Expand Up @@ -219,7 +235,7 @@ def _get_account_detail(self, account_name):
params = {
'account_name': account_name
}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)

data = parse_response(response)
Expand Down Expand Up @@ -601,7 +617,7 @@ def get_file_download_link(self, path):
"""
url = self._app_download_link_url()
params = {'path': path}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data.get('download_link')
Expand All @@ -612,7 +628,7 @@ def get_file_upload_link(self):
:return: dict
"""
url = self._app_upload_link_url()
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data
Expand Down Expand Up @@ -1231,7 +1247,7 @@ def get_custom_file_download_link(self, path):
"""
url = self._app_custom_download_link_url()
params = {'path': path}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data.get('download_link')
Expand All @@ -1240,7 +1256,7 @@ def get_custom_file_download_link(self, path):
def get_custom_file_upload_link(self, path):
url = self._app_custom_upload_link_url()
params = {'path': path}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data
Expand Down Expand Up @@ -1282,7 +1298,7 @@ def upload_local_file_to_custom_folder(self, local_path, custom_folder_path = No
def get_custom_file_info(self, path, name):
url = self._app_custom_asset_file_url()
params = {'path': path, 'name': name}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
d = data['dirent']
Expand All @@ -1301,7 +1317,7 @@ def get_custom_file_info(self, path, name):
def list_custom_assets(self, path):
url = self._app_custom_asset_dir_url()
params = {'path': path}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data
Expand All @@ -1310,7 +1326,7 @@ def list_custom_assets(self, path):
def get_user_info(self, username):
url = self._app_user_info_url()
params = {'username': username}
headers = parse_headers(self.token)
headers = self.headers
response = requests.get(url, params=params, headers=headers, timeout=self.timeout)
data = parse_response(response)
return data
Expand Down Expand Up @@ -1351,6 +1367,13 @@ def _get_temp_api_token_url(self, workspace_id, name):
'name': name
}

def _get_base_access_token_url(self, workspace_id, name):
return '%(server_url)s/api/v2.1/workspace/%(workspace_id)s/dtable/%(name)s/access-token/' % {
'server_url': self.server_url,
'workspace_id': workspace_id,
'name': name
}

@property
def token_headers(self):
return {
Expand Down Expand Up @@ -1407,9 +1430,21 @@ def copy_base(self, src_workspace_id, base_name, dst_workspace_id):
return parse_response(response).get('dtable')

def get_base(self, workspace_id, base_name, with_socket_io=False):
response = requests.get(self._get_temp_api_token_url(workspace_id, base_name),
headers=self.token_headers, timeout=self.timeout)
api_token = parse_response(response).get('api_token')
base = SeaTableAPI(api_token, self.server_url)
base.auth(with_socket_io=with_socket_io)
response = requests.get(self._get_base_access_token_url(workspace_id, base_name),
headers=self.token_headers,
timeout=self.timeout)
resp_json = response.json()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Warning] 保留统一响应解析的异常契约

影响:access-token 接口返回 401/403、Base 不存在、限流或服务错误时,直接解析并索引字段会把原 ConnectionError 变成 KeyError('access_token'),HTML 网关错误则变成 JSONDecodeError。403 Token expired. 也不再抛出 AuthExpiredError,已有调用方的重新登录和错误处理分支因此无法正常接管;精确 base/head 的回环 HTTP 对照已复现。

建议修复:改用现有 parse_response(response),成功后再读取字段,并补充过期、无权限、不存在和非 JSON 错误响应的回归。

access_token = resp_json['access_token']
dtable_uuid = resp_json['dtable_uuid']
base = SeaTableAPI('', self.server_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P1] 保留资产接口可用的认证方式

影响:Account.get_base 返回的 Base 现在 token 为空,但 get_file_upload_link、get_file_download_link 等方法仍以 self.token 请求 app-* 接口。隔离 HTTP 对照中网关查询正常,上传链接及 upload_bytes_file 却发送空 Token 凭据并返回 403;旧版上传链接成功。

建议修复:将这些方法同步迁移到支持 access-token 或账号认证的资源接口,保留授权范围;不能直接把 access JWT 当 API token,也不应恢复对已弃用 temp-api-token 的依赖。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

补充当前设计及配套服务端的修复方向:Web b58180ff 的 _resolve_api_token 已接受 dtable access JWT,因此本评论中“不能直接把 access JWT 当 API token”的笼统限制不再适用。SDK 60ab602 的空凭据问题仍可复现,且同样影响 custom assets、第三方凭据和用户信息;应让这些方法使用已初始化的 access JWT headers,并保留普通 API-token Base 的兼容行为,不恢复 temp-api-token 依赖。服务端必须先上线该兼容支持。Socket.IO 的刷新来源丢失仍是另一条独立问题。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本轮复核 head 8291dca:本线程的空资源凭据问题已修复。8 处资源方法均改用 self.headers;真实回环 HTTP 对照中,附件上传/下载、自定义资产、第三方账号和用户信息均发送 access JWT 并成功,旧审查版本 60ab602 在同一接收器下仍失败。上层文件操作及普通永久/temp-token Base 的 JWT 刷新兼容也已验证。不再将本线程列为当前阻塞;独立的 Socket.IO 续期来源问题和 get_base 错误解析问题仍存在。此结论限于本地协议与精确源码隔离验证,非线上集成验收。

base._auth(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] 为账号创建的 Base 保留续期能力

影响:这里仅初始化 access JWT,没有保留 Account 的刷新来源。SocketIO._on_io_disconnect 会无条件调用克隆 Base.auth(),后者仍拿空 self.token 请求 app-access-token;首次连接正常,但服务端主动断开后立即收到 403,无法重连,即使当前 JWT 尚未过期。

建议修复:保存 Account/workspace/Base 对应的 access-token 刷新提供者,让 auth() 和 Socket.IO 克隆通过它续期,并为普通 API-token Base 保留原行为;补充主动断开重连回归。

workspace_id,
base_name,
dtable_uuid,
access_token,
'',
'',
True,
with_socket_io=with_socket_io
)
return base
Loading