hyperpocket 0.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
hyperpocket/__init__.py
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
## Auth
|
2
|
+
|
3
|
+
각 provider에서 유저를 식별하기 위한 프로세스
|
4
|
+
|
5
|
+
### AuthContext
|
6
|
+
|
7
|
+
실제 유저의 인증 정보(e.g., access token)을 담고 있는 객체
|
8
|
+
|
9
|
+
```python
|
10
|
+
class AuthContext(BaseModel, ABC):
|
11
|
+
"""
|
12
|
+
This class is used to define the interface of the authentication model.
|
13
|
+
"""
|
14
|
+
access_token: str = Field(description="user's access token")
|
15
|
+
description: str = Field(description="description of this authentication context")
|
16
|
+
expires_at: Optional[datetime] = Field(description="expiration datetime")
|
17
|
+
detail: Optional[Any] = Field(default=None, description="detailed information")
|
18
|
+
```
|
19
|
+
|
20
|
+
일반적으로 해당 객체에서 access token의 key 정보나 response 객체를 AuthContext로 변환해주는 역할을 수행한다.
|
21
|
+
|
22
|
+
### AuthenticateRequest
|
23
|
+
|
24
|
+
authentication 수행 시에 필요한 정보를 담고 있는 객체
|
25
|
+
|
26
|
+
```python
|
27
|
+
class AuthenticateRequest(BaseModel):
|
28
|
+
auth_scopes: Optional[list[str]] = Field(default_factory=list,
|
29
|
+
description="authentication scopes. if the authentication handler is non scoped, it isn't needed")
|
30
|
+
```
|
31
|
+
|
32
|
+
- 일반적으로 인증 시에 필요한 client id 및 secret id 등과 같은 정보를 request에 같이 전달한다.
|
33
|
+
|
34
|
+
**examples**
|
35
|
+
|
36
|
+
```python
|
37
|
+
class GitHubOAuth2Request(AuthenticateRequest):
|
38
|
+
client_id: str
|
39
|
+
client_secret: str
|
40
|
+
```
|
41
|
+
|
42
|
+
### AuthenticateResponse(Optional)
|
43
|
+
|
44
|
+
authentication response에서 필수 정보를 담고 있는 객체
|
45
|
+
|
46
|
+
```python
|
47
|
+
class AuthenticateResponse(BaseModel):
|
48
|
+
"""
|
49
|
+
This class is used to define the interface of the authentication response.
|
50
|
+
"""
|
51
|
+
pass
|
52
|
+
```
|
53
|
+
|
54
|
+
- handler에서 response를 dict 형태로 처리한다면, 구현할 필요가 없다.
|
55
|
+
|
56
|
+
### AuthHandlerInterface
|
57
|
+
|
58
|
+
실제 authentication을 수행하는 객체
|
59
|
+
|
60
|
+
```python
|
61
|
+
class AuthHandlerInterface(ABC):
|
62
|
+
name: str = Field(description="name of the authentication handler")
|
63
|
+
description: str = Field(description="description of the authentication handler")
|
64
|
+
scoped: bool = Field(description="Indicates whether the handler requires an auth_scope for access control")
|
65
|
+
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
def provider() -> AuthProvider:
|
69
|
+
"""
|
70
|
+
Returns the authentication provider enum.
|
71
|
+
|
72
|
+
This method is used to determine the appropriate authentication handler
|
73
|
+
based on the authentication provider.
|
74
|
+
"""
|
75
|
+
raise NotImplementedError()
|
76
|
+
|
77
|
+
@staticmethod
|
78
|
+
def provider_default() -> bool:
|
79
|
+
"""
|
80
|
+
Indicates whether this authentication handler is the default handler.
|
81
|
+
|
82
|
+
If no specific handler is designated, the default handler will be used.
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
bool: True if this handler is the default, False otherwise.
|
86
|
+
"""
|
87
|
+
return False
|
88
|
+
|
89
|
+
@staticmethod
|
90
|
+
def recommended_scopes() -> set[str]:
|
91
|
+
"""
|
92
|
+
Returns the recommended authentication scopes.
|
93
|
+
|
94
|
+
If `use_recommended_scope` is set to True in the `AuthConfig`,
|
95
|
+
this method should return the proper recommended scopes. Otherwise,
|
96
|
+
it should return an empty set.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
set[str]: A set of recommended scopes, or an empty set if not applicable.
|
100
|
+
|
101
|
+
Examples:
|
102
|
+
Slack OAuth2 recommended_scopes::
|
103
|
+
|
104
|
+
def recommended_scopes() -> set[str]:
|
105
|
+
if config.auth.slack.use_recommended_scope:
|
106
|
+
recommended_scopes = {
|
107
|
+
"channels:history",
|
108
|
+
"channels:read",
|
109
|
+
"chat:write",
|
110
|
+
"groups:history",
|
111
|
+
"groups:read",
|
112
|
+
"im:history",
|
113
|
+
"mpim:history",
|
114
|
+
"reactions:read",
|
115
|
+
"reactions:write",
|
116
|
+
}
|
117
|
+
else:
|
118
|
+
recommended_scopes = {}
|
119
|
+
return recommended_scopes
|
120
|
+
"""
|
121
|
+
raise NotImplementedError()
|
122
|
+
|
123
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
|
124
|
+
"""
|
125
|
+
Make an AuthenticationRequest.
|
126
|
+
|
127
|
+
Usually, this method only requires `auth_scopes`.
|
128
|
+
If additional static information is needed (e.g., clientID, secretID),
|
129
|
+
retrieve it from the configuration.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
auth_scopes (Optional[list[str]]): list of auth scopes
|
133
|
+
|
134
|
+
Returns:
|
135
|
+
AuthenticateRequest: A authentication request object with the necessary details.
|
136
|
+
|
137
|
+
Examples:
|
138
|
+
Create a Slack OAuth2 request::
|
139
|
+
|
140
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
|
141
|
+
return SlackOAuth2Request(
|
142
|
+
auth_scopes=auth_scopes,
|
143
|
+
client_id=config.auth.slack.client_id,
|
144
|
+
client_secret=config.auth.slack.client_secret
|
145
|
+
)
|
146
|
+
"""
|
147
|
+
raise NotImplementedError()
|
148
|
+
|
149
|
+
def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str,
|
150
|
+
future_uid: str, *args, **kwargs) -> str:
|
151
|
+
"""
|
152
|
+
Performs preliminary tasks required for authentication.
|
153
|
+
|
154
|
+
This method typically performs the following actions:
|
155
|
+
- Creates a future to wait for user authentication completion during the authentication process.
|
156
|
+
- Issues an authentication URI that the user can access.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
160
|
+
thread_id (str): The thread ID.
|
161
|
+
profile (str): The profile name.
|
162
|
+
future_uid (str): A unique identifier for each future.
|
163
|
+
|
164
|
+
Returns:
|
165
|
+
str: The authentication URI that the user can access.
|
166
|
+
"""
|
167
|
+
raise NotImplementedError()
|
168
|
+
|
169
|
+
@abstractmethod
|
170
|
+
async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
|
171
|
+
"""
|
172
|
+
Performs the actual authentication process.
|
173
|
+
|
174
|
+
This function assumes that the user has completed the authentication during the `prepare` step,
|
175
|
+
and the associated future has been resolved. At this point, the result contains the required
|
176
|
+
values for authentication (e.g., an auth code).
|
177
|
+
|
178
|
+
Typically, this process involves:
|
179
|
+
- Accessing the resolved future to retrieve the necessary values for authentication.
|
180
|
+
- Performing the actual authentication using these values.
|
181
|
+
- Converting the returned response into an appropriate `AuthContext` object and returning it.
|
182
|
+
|
183
|
+
Args:
|
184
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
185
|
+
future_uid (str): A unique identifier for the future, used to retrieve the correct
|
186
|
+
result issued during the `prepare` step.
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
AuthContext: The authentication context object containing the authentication result.
|
190
|
+
"""
|
191
|
+
raise NotImplementedError()
|
192
|
+
|
193
|
+
@abstractmethod
|
194
|
+
async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
|
195
|
+
"""
|
196
|
+
Performs re-authentication for an expired session.
|
197
|
+
|
198
|
+
This method is optional and does not need to be implemented for handlers that do not require re-authentication.
|
199
|
+
|
200
|
+
Typically, the information needed for re-authentication (e.g., a refresh token) should be stored
|
201
|
+
within the `AuthContext` during the previous authentication step.
|
202
|
+
In the `refresh` step, this method accesses the necessary re-authentication details from the provided `context`,
|
203
|
+
performs the re-authentication, and returns an updated `AuthContext`.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
207
|
+
context (AuthContext): The current authentication context that it should contain data required for re-authentication.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
AuthContext: An updated authentication context object.
|
211
|
+
"""
|
212
|
+
raise NotImplementedError()
|
213
|
+
```
|
214
|
+
|
215
|
+
## How To Implement Auth
|
216
|
+
|
217
|
+
1. `AuthenticateRequest`을 상속받은 구현체 구현
|
218
|
+
- 해당 클래스에서는 실제 prepare 및 authenticate를 할 때 필요한 정보를 저장하고 있어야 합니다.
|
219
|
+
|
220
|
+
2. `AuthContext`을 상속받은 구현체 구현
|
221
|
+
- Context 내에는 다음과 같은 정보들이 존재해야 합니다.
|
222
|
+
- 각 tool에서 접근할 수 있는 access_key
|
223
|
+
- response를 context로 변환할 수 있는 함수
|
224
|
+
|
225
|
+
3. `Response`(Optional) 객체 구현
|
226
|
+
- 필요시 authentication을 완료 후 받은 정보를 parsing할 구현체를 구현
|
227
|
+
|
228
|
+
4. `AuthHandlerInterface`을 상속받은 구현체 구현
|
229
|
+
- 실제 prepare, authentication, refresh를 수행할 객체를 구현합니다.
|
230
|
+
|
231
|
+
5. (신규 auth provider인 경우) `AuthProvider`에 새로운 enum value를 추가
|
232
|
+
- 만약 새로운 `AuthProvider`가 추가되는 경우라면, `pocket/auth/provider.py` 내에 새로운 AuthProvider Enum을 등록해야 합니다.
|
233
|
+
|
234
|
+
6. 신규 Auth callback server enpoint 추가
|
235
|
+
- `pocket/server/auth/` 하위에 유저가 authentication을 완료하면 callback될 server endpoint를 추가해야 합니다.
|
236
|
+
- 해당 패키지 하위에 적절한 `APIRouter`를 선언해놓으면 pocket 초기화 시에 자동으로 end point를 pocket server에 등록해주게 됩니다.
|
237
|
+
|
238
|
+
7. Auth Test code 추가(Optional)
|
239
|
+
- `pocket/auth/tests` 하위에 테스트 코드 추가
|
240
|
+
|
241
|
+
---
|
242
|
+
|
243
|
+
## Auth Flow(Advanced)
|
244
|
+
|
245
|
+
### Session State 정의
|
246
|
+
|
247
|
+
- SKIP_AUTH : Auth가 존재하고, 들어온 요청에 대한 권한도 가지고 있는 상태
|
248
|
+
- DO_AUTH : Auth가 존재하지만, 들어온 요청에 대한 권한이 없기 때문에 auth가 필요한 상태
|
249
|
+
- DO_REFRESH : Auth가 존재하지만, 만료가 되어 refresh가 필요한 상태
|
250
|
+
- NO_SESSION : Auth가 존재하지 않는 상태
|
251
|
+
- PENDING_RESOLVE : Resource Owner에게 authorization URI를 전달하고 인증을 기다리는 상태
|
252
|
+
- RESOLVED : Resource Owner가 authorization을 완료해 서버에서 code를 받은 상태
|
253
|
+
|
254
|
+
### 01. check
|
255
|
+
|
256
|
+
처음 요청이 들어왔을 때 현재 사용자의 session이 어느 상태인지를 check하게 된다.
|
257
|
+
|
258
|
+
1. 사용자의 현재 session 조회
|
259
|
+
- 존재 하지 않으면 NO_SESSION 상태 반환
|
260
|
+
2. session의 auth resolve uid가 존재하는지 확인
|
261
|
+
- auth resolve uid가 아직 존재하는 것은 사용자에게 authentication URI는 전달하였지만 서버에서 access token을 아직 얻지는 못 한 상태
|
262
|
+
- auth resolve uid가 존재하면서 현재 사용자가 인증을 완료한 상태인지 확인
|
263
|
+
- 사용자가 인증을 완료한 상태라면 RESOLVED 상태 반환
|
264
|
+
- 사용자가 인증을 완료하지 못 한 상태라면 PENDING_RESOLVE 상태 반환
|
265
|
+
3. 현재 인증된 session이 현재 요청에 적용이 가능한지를 확인
|
266
|
+
- 현재 session의 auth provider와 필요한 session의 auth provider가 동일한지를 확인
|
267
|
+
- 다르다면 반드시 재인증이 필요
|
268
|
+
- 현재 session을 인증한 auto provider가 non scoped인지 확인
|
269
|
+
- scoped가 아닌 경우에는 모든 권한이 있기 때문에 항상 사용 가능
|
270
|
+
- scoped인 경우 필요한 scope을 이미 가지고 있는지 확인
|
271
|
+
- 이미 가지고 있는 경우 그대로 사용하면 된다.
|
272
|
+
- 가지고 있지 않은 경우 새로운 인증 요청이 필요. 이때 기존 갖고 있던 scope과 새로 필요한 scope의 합집합을 다시 요청하게 된다.
|
273
|
+
- 위 과정을 통해 현재 session에서 적용이 불가능 하다고 판단되면 DO_AUTH 상태 반환
|
274
|
+
4. 현재 인증된 session이 만료되었는지 확인
|
275
|
+
- 만료되었다면 DO_REFRESH 상태 반환
|
276
|
+
5. 위 단계를 모두 통과한 경우에만 SKIP_AUTH 상태 반환
|
277
|
+
|
278
|
+
### 02. prepare
|
279
|
+
|
280
|
+
1. auth state가 SKIP_AUTH, DO_REFRESH, RESOLVED인 경우에는 그대로 함수를 종료
|
281
|
+
2. DO_AUTH, NO_SESSION인 경우에는 새로운 session과 future를 만들고, 사용자에게 authenticate URI를 반환
|
282
|
+
3. PENDING_RESOLVED인 경우에는 기존 session의 authenticate URI를 다시 반환
|
283
|
+
|
284
|
+
```mermaid
|
285
|
+
flowchart TD
|
286
|
+
Check["Prepare Auth"] --> A["Is AuthState<br>SKIP_AUTH, DO_REFRESH or RESOLVED?"]
|
287
|
+
A -->|Yes| B["Return<br>None"]
|
288
|
+
A -->|No| C["Is AuthState PENDING_RESOLVED?"]
|
289
|
+
C -->|YES| D["Return<br>Previous Authorization URI"]
|
290
|
+
C -->|NO| E["Make New Session"]
|
291
|
+
E --> F["Return<br>Authorization URI"]
|
292
|
+
```
|
293
|
+
|
294
|
+
### 03. Authenticate
|
295
|
+
|
296
|
+
1. auth state가 SKIP_AUTH인 경우에는 기존 session을 그대로 반환
|
297
|
+
2. auth state가 DO_REFRESH인 경우에는 refresh를 수행하고 기존 session을 교체
|
298
|
+
3. auth state가 PENDING_RESOLVE 또는 RESOLVED인 경우에는 사용자로부터 전달받은 code로 authenticate를 수행
|
299
|
+
4. 그 외 auth state(NO_SESSION, DO_AUTH)는 해당 단계로 진입하지 않는다.
|
300
|
+
|
301
|
+
```mermaid
|
302
|
+
flowchart TD
|
303
|
+
Check["Authenticate"]
|
304
|
+
Check -->|Is AuthState<br>SKIP_AUTH?| A2["Return<br>Existing Session"]
|
305
|
+
Check -->|Is AuthState<br>DO_REFRESH?| B2["Refresh<br>Access Token"]
|
306
|
+
Check -->|Is AuthState PENDING_RESOLVE<br>or<br>RESOLVED?| C2["Wait for Code<br>And<br>Authenticate"]
|
307
|
+
Check -->|Is AuthState<br>NO_SESION<br>or<br>DO_AUTH?| D2["Can't be reached<br>while in state"]
|
308
|
+
```
|
309
|
+
|
@@ -0,0 +1,323 @@
|
|
1
|
+
# Auth
|
2
|
+
|
3
|
+
A Process to identify users for each provider.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## Interface
|
8
|
+
|
9
|
+
### AuthContext
|
10
|
+
|
11
|
+
An object containing the actual authentication information of the user (e.g., access token).
|
12
|
+
|
13
|
+
```python
|
14
|
+
class AuthContext(BaseModel, ABC):
|
15
|
+
"""
|
16
|
+
This class is used to define the interface of the authentication model.
|
17
|
+
"""
|
18
|
+
access_token: str = Field(description="user's access token")
|
19
|
+
description: str = Field(description="description of this authentication context")
|
20
|
+
expires_at: Optional[datetime] = Field(description="expiration datetime")
|
21
|
+
detail: Optional[Any] = Field(default=None, description="detailed information")
|
22
|
+
```
|
23
|
+
|
24
|
+
- The primary purpose of this object is to transform key details, such as the access token or response object, into an
|
25
|
+
AuthContext.
|
26
|
+
|
27
|
+
### AuthenticateRequest
|
28
|
+
|
29
|
+
An object containing the necessary information to perform authentication.
|
30
|
+
|
31
|
+
```python
|
32
|
+
class AuthenticateRequest(BaseModel):
|
33
|
+
auth_scopes: Optional[list[str]] = Field(
|
34
|
+
default_factory=list,
|
35
|
+
description="authentication scopes. if the authentication handler is non scoped, it isn't needed")
|
36
|
+
```
|
37
|
+
|
38
|
+
- Typically, this object also includes information like client ID and secret ID required for authentication
|
39
|
+
|
40
|
+
**examples**
|
41
|
+
|
42
|
+
```python
|
43
|
+
class GitHubOAuth2Request(AuthenticateRequest):
|
44
|
+
client_id: str
|
45
|
+
client_secret: str
|
46
|
+
```
|
47
|
+
|
48
|
+
### AuthenticateResponse(Optional)
|
49
|
+
|
50
|
+
An object containing the necessary information from authentication response.
|
51
|
+
|
52
|
+
```python
|
53
|
+
class AuthenticateResponse(BaseModel):
|
54
|
+
"""
|
55
|
+
This class is used to define the interface of the authentication response.
|
56
|
+
"""
|
57
|
+
pass
|
58
|
+
```
|
59
|
+
|
60
|
+
- If the response is handled as dict in handler, don't have to implement this.
|
61
|
+
|
62
|
+
### AuthHandlerInterface
|
63
|
+
|
64
|
+
An interface for the object that performs the actual authentication process.
|
65
|
+
|
66
|
+
```python
|
67
|
+
class AuthHandlerInterface(ABC):
|
68
|
+
name: str = Field(description="name of the authentication handler")
|
69
|
+
description: str = Field(description="description of the authentication handler")
|
70
|
+
scoped: bool = Field(description="Indicates whether the handler requires an auth_scope for access control")
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
def provider() -> AuthProvider:
|
74
|
+
"""
|
75
|
+
Returns the authentication provider enum.
|
76
|
+
|
77
|
+
This method is used to determine the appropriate authentication handler
|
78
|
+
based on the authentication provider.
|
79
|
+
"""
|
80
|
+
raise NotImplementedError()
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
def provider_default() -> bool:
|
84
|
+
"""
|
85
|
+
Indicates whether this authentication handler is the default handler.
|
86
|
+
|
87
|
+
If no specific handler is designated, the default handler will be used.
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
bool: True if this handler is the default, False otherwise.
|
91
|
+
"""
|
92
|
+
return False
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
def recommended_scopes() -> set[str]:
|
96
|
+
"""
|
97
|
+
Returns the recommended authentication scopes.
|
98
|
+
|
99
|
+
If `use_recommended_scope` is set to True in the `AuthConfig`,
|
100
|
+
this method should return the proper recommended scopes. Otherwise,
|
101
|
+
it should return an empty set.
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
set[str]: A set of recommended scopes, or an empty set if not applicable.
|
105
|
+
|
106
|
+
Examples:
|
107
|
+
Slack OAuth2 recommended_scopes::
|
108
|
+
|
109
|
+
def recommended_scopes() -> set[str]:
|
110
|
+
if config.auth.slack.use_recommended_scope:
|
111
|
+
recommended_scopes = {
|
112
|
+
"channels:history",
|
113
|
+
"channels:read",
|
114
|
+
"chat:write",
|
115
|
+
"groups:history",
|
116
|
+
"groups:read",
|
117
|
+
"im:history",
|
118
|
+
"mpim:history",
|
119
|
+
"reactions:read",
|
120
|
+
"reactions:write",
|
121
|
+
}
|
122
|
+
else:
|
123
|
+
recommended_scopes = {}
|
124
|
+
return recommended_scopes
|
125
|
+
"""
|
126
|
+
raise NotImplementedError()
|
127
|
+
|
128
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
|
129
|
+
"""
|
130
|
+
Make an AuthenticationRequest.
|
131
|
+
|
132
|
+
Usually, this method only requires `auth_scopes`.
|
133
|
+
If additional static information is needed (e.g., clientID, secretID),
|
134
|
+
retrieve it from the configuration.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
auth_scopes (Optional[list[str]]): list of auth scopes
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
AuthenticateRequest: A authentication request object with the necessary details.
|
141
|
+
|
142
|
+
Examples:
|
143
|
+
Create a Slack OAuth2 request::
|
144
|
+
|
145
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
|
146
|
+
return SlackOAuth2Request(
|
147
|
+
auth_scopes=auth_scopes,
|
148
|
+
client_id=config.auth.slack.client_id,
|
149
|
+
client_secret=config.auth.slack.client_secret
|
150
|
+
)
|
151
|
+
"""
|
152
|
+
raise NotImplementedError()
|
153
|
+
|
154
|
+
def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str,
|
155
|
+
future_uid: str, *args, **kwargs) -> str:
|
156
|
+
"""
|
157
|
+
Performs preliminary tasks required for authentication.
|
158
|
+
|
159
|
+
This method typically performs the following actions:
|
160
|
+
- Creates a future to wait for user authentication completion during the authentication process.
|
161
|
+
- Issues an authentication URI that the user can access.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
165
|
+
thread_id (str): The thread ID.
|
166
|
+
profile (str): The profile name.
|
167
|
+
future_uid (str): A unique identifier for each future.
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
str: The authentication URI that the user can access.
|
171
|
+
"""
|
172
|
+
raise NotImplementedError()
|
173
|
+
|
174
|
+
@abstractmethod
|
175
|
+
async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
|
176
|
+
"""
|
177
|
+
Performs the actual authentication process.
|
178
|
+
|
179
|
+
This function assumes that the user has completed the authentication during the `prepare` step,
|
180
|
+
and the associated future has been resolved. At this point, the result contains the required
|
181
|
+
values for authentication (e.g., an auth code).
|
182
|
+
|
183
|
+
Typically, this process involves:
|
184
|
+
- Accessing the resolved future to retrieve the necessary values for authentication.
|
185
|
+
- Performing the actual authentication using these values.
|
186
|
+
- Converting the returned response into an appropriate `AuthContext` object and returning it.
|
187
|
+
|
188
|
+
Args:
|
189
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
190
|
+
future_uid (str): A unique identifier for the future, used to retrieve the correct
|
191
|
+
result issued during the `prepare` step.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
AuthContext: The authentication context object containing the authentication result.
|
195
|
+
"""
|
196
|
+
raise NotImplementedError()
|
197
|
+
|
198
|
+
@abstractmethod
|
199
|
+
async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
|
200
|
+
"""
|
201
|
+
Performs re-authentication for an expired session.
|
202
|
+
|
203
|
+
This method is optional and does not need to be implemented for handlers that do not require re-authentication.
|
204
|
+
|
205
|
+
Typically, the information needed for re-authentication (e.g., a refresh token) should be stored
|
206
|
+
within the `AuthContext` during the previous authentication step.
|
207
|
+
In the `refresh` step, this method accesses the necessary re-authentication details from the provided `context`,
|
208
|
+
performs the re-authentication, and returns an updated `AuthContext`.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
auth_req (AuthenticateRequest): The authentication request object.
|
212
|
+
context (AuthContext): The current authentication context that it should contain data required for re-authentication.
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
AuthContext: An updated authentication context object.
|
216
|
+
"""
|
217
|
+
raise NotImplementedError()
|
218
|
+
```
|
219
|
+
|
220
|
+
---
|
221
|
+
|
222
|
+
## How To Implement Auth
|
223
|
+
|
224
|
+
1. Implement a class that inherits `AuthenticateRequest`
|
225
|
+
- This class should store the necessary information for the prepare and authenticate steps.
|
226
|
+
|
227
|
+
2. Implement a class that inherits `AuthContext`
|
228
|
+
- This Context should include:
|
229
|
+
- The access key that tools can access.
|
230
|
+
- A function to convert a response into an `AuthContext`
|
231
|
+
|
232
|
+
3. Optionally, implement a `Response` class
|
233
|
+
- This class can parse the response data received after completing authentication.
|
234
|
+
|
235
|
+
4. Implement a class that inherits `AuthHandlerInterface`
|
236
|
+
- This class perform the actual prepare, authenticate, and refresh steps.
|
237
|
+
|
238
|
+
5. Add a new enum value to the `AuthProvider` Enum(only implement new auth provider)
|
239
|
+
- If a new `AuthProvider` is added, update the `AuthProvider` enum in `pocket/auth/provider.py`
|
240
|
+
|
241
|
+
6. Add new auth callback server endpoint
|
242
|
+
- Add the endpoint under `pocket/server/auth/` packages.
|
243
|
+
- Declare an appropriate `APIRouter` in the package. Pocket will automatically register the endpoint during
|
244
|
+
initialization.
|
245
|
+
|
246
|
+
7. Add test code(optional)
|
247
|
+
- Add the test code under `pocket/auth/tests/` packages.
|
248
|
+
|
249
|
+
---
|
250
|
+
|
251
|
+
## Auth Flow(Advanced)
|
252
|
+
|
253
|
+
### Session States
|
254
|
+
|
255
|
+
- SKIP_AUTH: The authentication exists, and the request has the necessary permissions.
|
256
|
+
|
257
|
+
- DO_AUTH: Authentication exists, but the request requires additional permissions.
|
258
|
+
|
259
|
+
- DO_REFRESH: Authentication exists but has expired, requiring a refresh.
|
260
|
+
|
261
|
+
- NO_SESSION: No authentication exists.
|
262
|
+
|
263
|
+
- PENDING_RESOLVE: Waiting for the user to complete the authentication process via the authorization URI.
|
264
|
+
|
265
|
+
- RESOLVED: The user has completed authentication, and the server has received the authorization code.
|
266
|
+
|
267
|
+
### 01. check
|
268
|
+
|
269
|
+
Determines the current session state when a request is received.
|
270
|
+
|
271
|
+
1. Check if a session exists.
|
272
|
+
- If not, return NO_SESSION.
|
273
|
+
|
274
|
+
2. Check for an auth resolve uid.
|
275
|
+
- If it exists but authentication isn’t complete, return PENDING_RESOLVE.
|
276
|
+
- If completed, return RESOLVED.
|
277
|
+
|
278
|
+
3. Check if the session is valid for the request:
|
279
|
+
- Compare the auth provider.
|
280
|
+
- Validate scopes (for scoped providers).
|
281
|
+
- If the session doesn’t satisfy requirements, return DO_AUTH.
|
282
|
+
|
283
|
+
4. Check if the session has expired.
|
284
|
+
- If expired, return DO_REFRESH.
|
285
|
+
|
286
|
+
5. If all checks pass, return SKIP_AUTH.
|
287
|
+
|
288
|
+
### 02. prepare
|
289
|
+
|
290
|
+
1. If the auth state is SKIP_AUTH, DO_REFRESH, or RESOLVED, do nothing.
|
291
|
+
|
292
|
+
2. If the auth state is DO_AUTH or NO_SESSION:
|
293
|
+
- Create a new session and future.
|
294
|
+
- Return the authorization URI.
|
295
|
+
|
296
|
+
3. If the state is PENDING_RESOLVE, return the existing authorization URI.
|
297
|
+
|
298
|
+
```mermaid
|
299
|
+
flowchart TD
|
300
|
+
Check["Prepare Auth"] --> A["Is AuthState<br>SKIP_AUTH, DO_REFRESH or RESOLVED?"]
|
301
|
+
A -->|Yes| B["Return<br>None"]
|
302
|
+
A -->|No| C["Is AuthState PENDING_RESOLVED?"]
|
303
|
+
C -->|YES| D["Return<br>Previous Authorization URI"]
|
304
|
+
C -->|NO| E["Make New Session"]
|
305
|
+
E --> F["Return<br>Authorization URI"]
|
306
|
+
```
|
307
|
+
|
308
|
+
### 03. Authenticate
|
309
|
+
|
310
|
+
1. If the auth state is SKIP_AUTH, return the existing session.
|
311
|
+
2. If the state is DO_REFRESH, refresh the session.
|
312
|
+
3. If the state is PENDING_RESOLVE or RESOLVED, perform authentication using the authorization code.
|
313
|
+
4. If the state is NO_SESSION or DO_AUTH, this step should not be reached.
|
314
|
+
|
315
|
+
```mermaid
|
316
|
+
flowchart TD
|
317
|
+
Check["Authenticate"]
|
318
|
+
Check -->|Is AuthState<br>SKIP_AUTH?| A2["Return<br>Existing Session"]
|
319
|
+
Check -->|Is AuthState<br>DO_REFRESH?| B2["Refresh<br>Access Token"]
|
320
|
+
Check -->|Is AuthState PENDING_RESOLVE<br>or<br>RESOLVED?| C2["Wait for Code<br>And<br>Authenticate"]
|
321
|
+
Check -->|Is AuthState<br>NO_SESION<br>or<br>DO_AUTH?| D2["Can't be reached<br>while in state"]
|
322
|
+
```
|
323
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
from hyperpocket.auth.github.context import GitHubAuthContext
|
3
|
+
from hyperpocket.auth.github.oauth2_context import GitHubOAuth2AuthContext
|
4
|
+
from hyperpocket.auth.github.token_context import GitHubTokenAuthContext
|
5
|
+
from hyperpocket.auth.google.context import GoogleAuthContext
|
6
|
+
from hyperpocket.auth.google.oauth2_context import GoogleOAuth2AuthContext
|
7
|
+
from hyperpocket.auth.handler import AuthHandlerInterface
|
8
|
+
from hyperpocket.auth.linear.token_context import LinearTokenAuthContext
|
9
|
+
from hyperpocket.auth.provider import AuthProvider
|
10
|
+
from hyperpocket.auth.slack.oauth2_context import SlackOAuth2AuthContext
|
11
|
+
from hyperpocket.auth.slack.token_context import SlackTokenAuthContext
|
12
|
+
from hyperpocket.util.find_all_leaf_class_in_package import find_all_leaf_class_in_package
|
13
|
+
|
14
|
+
PREBUILT_AUTH_HANDLERS = find_all_leaf_class_in_package("hyperpocket.auth", AuthHandlerInterface)
|
15
|
+
AUTH_CONTEXT_MAP = {
|
16
|
+
leaf.__name__: leaf for leaf in find_all_leaf_class_in_package("hyperpocket.auth", AuthContext)
|
17
|
+
}
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
'PREBUILT_AUTH_HANDLERS',
|
21
|
+
'AUTH_CONTEXT_MAP',
|
22
|
+
'AuthProvider',
|
23
|
+
'AuthHandlerInterface',
|
24
|
+
]
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
|
3
|
+
|
4
|
+
class CalendlyAuthContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "CALENDLY_TOKEN"
|
6
|
+
|
7
|
+
def to_dict(self) -> dict[str, str]:
|
8
|
+
return {self._ACCESS_TOKEN_KEY: self.access_token}
|
9
|
+
|
10
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
11
|
+
return {
|
12
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
13
|
+
}
|