kaappu-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kaappu_sdk-0.1.0/LICENSE +21 -0
- kaappu_sdk-0.1.0/PKG-INFO +129 -0
- kaappu_sdk-0.1.0/README.md +111 -0
- kaappu_sdk-0.1.0/kaappu/__init__.py +14 -0
- kaappu_sdk-0.1.0/kaappu/client.py +71 -0
- kaappu_sdk-0.1.0/kaappu/context.py +33 -0
- kaappu_sdk-0.1.0/kaappu/decorators.py +45 -0
- kaappu_sdk-0.1.0/kaappu/permissions.py +37 -0
- kaappu_sdk-0.1.0/kaappu_sdk.egg-info/PKG-INFO +129 -0
- kaappu_sdk-0.1.0/kaappu_sdk.egg-info/SOURCES.txt +14 -0
- kaappu_sdk-0.1.0/kaappu_sdk.egg-info/dependency_links.txt +1 -0
- kaappu_sdk-0.1.0/kaappu_sdk.egg-info/requires.txt +11 -0
- kaappu_sdk-0.1.0/kaappu_sdk.egg-info/top_level.txt +1 -0
- kaappu_sdk-0.1.0/pyproject.toml +23 -0
- kaappu_sdk-0.1.0/setup.cfg +4 -0
- kaappu_sdk-0.1.0/tests/test_permissions.py +65 -0
kaappu_sdk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaappu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaappu-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JWT authentication and permission-based authorization for Python services
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: PyJWT[crypto]>=2.8.0
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Provides-Extra: flask
|
|
12
|
+
Requires-Dist: Flask>=2.3.0; extra == "flask"
|
|
13
|
+
Provides-Extra: fastapi
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# kaappu-sdk
|
|
20
|
+
|
|
21
|
+
JWT authentication and permission-based authorization for Python services. Works with Flask, FastAPI, or any Python HTTP framework.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install kaappu-sdk
|
|
27
|
+
|
|
28
|
+
# With Flask support
|
|
29
|
+
pip install kaappu-sdk[flask]
|
|
30
|
+
|
|
31
|
+
# With FastAPI support
|
|
32
|
+
pip install kaappu-sdk[fastapi]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Permission Checking
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from kaappu import check_permission, check_all_permissions, check_any_permission
|
|
41
|
+
|
|
42
|
+
user_perms = ["users:read", "roles:read", "gateway:*"]
|
|
43
|
+
|
|
44
|
+
check_permission(user_perms, "users:read") # True
|
|
45
|
+
check_permission(user_perms, "users:delete") # False
|
|
46
|
+
check_permission(user_perms, "gateway:view") # True (wildcard)
|
|
47
|
+
check_all_permissions(user_perms, ["users:read", "roles:read"]) # True
|
|
48
|
+
check_any_permission(user_perms, ["users:delete", "roles:read"]) # True
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Wildcard support:
|
|
52
|
+
- `*` -- super wildcard, matches any permission
|
|
53
|
+
- `resource:*` -- matches any action on that resource
|
|
54
|
+
|
|
55
|
+
### Flask Route Protection
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from flask import Flask
|
|
59
|
+
from kaappu.decorators import require_permission
|
|
60
|
+
|
|
61
|
+
app = Flask(__name__)
|
|
62
|
+
|
|
63
|
+
@app.route("/api/users")
|
|
64
|
+
@require_permission("users:read")
|
|
65
|
+
def list_users():
|
|
66
|
+
return {"users": []}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Security Context
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from kaappu import SecurityContext
|
|
73
|
+
from kaappu.context import set_context, get_context
|
|
74
|
+
|
|
75
|
+
# Set context (typically in middleware)
|
|
76
|
+
ctx = SecurityContext(
|
|
77
|
+
user_id="u_123",
|
|
78
|
+
account_id="acc_456",
|
|
79
|
+
email="user@example.com",
|
|
80
|
+
permissions=["users:read", "roles:read"],
|
|
81
|
+
)
|
|
82
|
+
set_context(ctx)
|
|
83
|
+
|
|
84
|
+
# Read context anywhere in the same thread
|
|
85
|
+
current = get_context()
|
|
86
|
+
print(current.email)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### API Client
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from kaappu import KaappuClient
|
|
93
|
+
|
|
94
|
+
client = KaappuClient(
|
|
95
|
+
base_url="https://your-kaappu-instance",
|
|
96
|
+
publishable_key="pk_live_...",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Sign in
|
|
100
|
+
result = client.sign_in("user@example.com", "password")
|
|
101
|
+
access_token = result["accessToken"]
|
|
102
|
+
|
|
103
|
+
# Get current user
|
|
104
|
+
user = client.get_me(access_token)
|
|
105
|
+
|
|
106
|
+
# Refresh token
|
|
107
|
+
new_tokens = client.refresh_token(result["refreshToken"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
| Function / Class | Description |
|
|
113
|
+
|------------------|-------------|
|
|
114
|
+
| `check_permission(perms, required)` | Check a single permission with wildcard support |
|
|
115
|
+
| `check_all_permissions(perms, required)` | Check that ALL permissions are satisfied |
|
|
116
|
+
| `check_any_permission(perms, required)` | Check that ANY permission is satisfied |
|
|
117
|
+
| `SecurityContext` | Dataclass holding user identity and permissions |
|
|
118
|
+
| `set_context(ctx)` / `get_context()` | Thread-local security context storage |
|
|
119
|
+
| `require_permission(perm)` | Flask route decorator for permission enforcement |
|
|
120
|
+
| `KaappuClient` | API client for sign-in, sign-up, refresh, and user fetch |
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- Python 3.9+
|
|
125
|
+
- PyJWT 2.8+
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# kaappu-sdk
|
|
2
|
+
|
|
3
|
+
JWT authentication and permission-based authorization for Python services. Works with Flask, FastAPI, or any Python HTTP framework.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kaappu-sdk
|
|
9
|
+
|
|
10
|
+
# With Flask support
|
|
11
|
+
pip install kaappu-sdk[flask]
|
|
12
|
+
|
|
13
|
+
# With FastAPI support
|
|
14
|
+
pip install kaappu-sdk[fastapi]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Permission Checking
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from kaappu import check_permission, check_all_permissions, check_any_permission
|
|
23
|
+
|
|
24
|
+
user_perms = ["users:read", "roles:read", "gateway:*"]
|
|
25
|
+
|
|
26
|
+
check_permission(user_perms, "users:read") # True
|
|
27
|
+
check_permission(user_perms, "users:delete") # False
|
|
28
|
+
check_permission(user_perms, "gateway:view") # True (wildcard)
|
|
29
|
+
check_all_permissions(user_perms, ["users:read", "roles:read"]) # True
|
|
30
|
+
check_any_permission(user_perms, ["users:delete", "roles:read"]) # True
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Wildcard support:
|
|
34
|
+
- `*` -- super wildcard, matches any permission
|
|
35
|
+
- `resource:*` -- matches any action on that resource
|
|
36
|
+
|
|
37
|
+
### Flask Route Protection
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from flask import Flask
|
|
41
|
+
from kaappu.decorators import require_permission
|
|
42
|
+
|
|
43
|
+
app = Flask(__name__)
|
|
44
|
+
|
|
45
|
+
@app.route("/api/users")
|
|
46
|
+
@require_permission("users:read")
|
|
47
|
+
def list_users():
|
|
48
|
+
return {"users": []}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Security Context
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from kaappu import SecurityContext
|
|
55
|
+
from kaappu.context import set_context, get_context
|
|
56
|
+
|
|
57
|
+
# Set context (typically in middleware)
|
|
58
|
+
ctx = SecurityContext(
|
|
59
|
+
user_id="u_123",
|
|
60
|
+
account_id="acc_456",
|
|
61
|
+
email="user@example.com",
|
|
62
|
+
permissions=["users:read", "roles:read"],
|
|
63
|
+
)
|
|
64
|
+
set_context(ctx)
|
|
65
|
+
|
|
66
|
+
# Read context anywhere in the same thread
|
|
67
|
+
current = get_context()
|
|
68
|
+
print(current.email)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### API Client
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from kaappu import KaappuClient
|
|
75
|
+
|
|
76
|
+
client = KaappuClient(
|
|
77
|
+
base_url="https://your-kaappu-instance",
|
|
78
|
+
publishable_key="pk_live_...",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Sign in
|
|
82
|
+
result = client.sign_in("user@example.com", "password")
|
|
83
|
+
access_token = result["accessToken"]
|
|
84
|
+
|
|
85
|
+
# Get current user
|
|
86
|
+
user = client.get_me(access_token)
|
|
87
|
+
|
|
88
|
+
# Refresh token
|
|
89
|
+
new_tokens = client.refresh_token(result["refreshToken"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
| Function / Class | Description |
|
|
95
|
+
|------------------|-------------|
|
|
96
|
+
| `check_permission(perms, required)` | Check a single permission with wildcard support |
|
|
97
|
+
| `check_all_permissions(perms, required)` | Check that ALL permissions are satisfied |
|
|
98
|
+
| `check_any_permission(perms, required)` | Check that ANY permission is satisfied |
|
|
99
|
+
| `SecurityContext` | Dataclass holding user identity and permissions |
|
|
100
|
+
| `set_context(ctx)` / `get_context()` | Thread-local security context storage |
|
|
101
|
+
| `require_permission(perm)` | Flask route decorator for permission enforcement |
|
|
102
|
+
| `KaappuClient` | API client for sign-in, sign-up, refresh, and user fetch |
|
|
103
|
+
|
|
104
|
+
## Requirements
|
|
105
|
+
|
|
106
|
+
- Python 3.9+
|
|
107
|
+
- PyJWT 2.8+
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Kaappu SDK for Python — JWT authentication and permission-based authorization."""
|
|
2
|
+
|
|
3
|
+
from kaappu.permissions import check_permission, check_all_permissions, check_any_permission
|
|
4
|
+
from kaappu.context import SecurityContext
|
|
5
|
+
from kaappu.client import KaappuClient
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__all__ = [
|
|
9
|
+
"check_permission",
|
|
10
|
+
"check_all_permissions",
|
|
11
|
+
"check_any_permission",
|
|
12
|
+
"SecurityContext",
|
|
13
|
+
"KaappuClient",
|
|
14
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""API client for Kaappu Identity — sign in, sign up, refresh, verify."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KaappuClient:
|
|
8
|
+
"""Framework-agnostic client for Kaappu Identity APIs."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, base_url: str = "http://localhost:9091", publishable_key: str = ""):
|
|
11
|
+
self.base_url = base_url.rstrip("/")
|
|
12
|
+
self.publishable_key = publishable_key
|
|
13
|
+
|
|
14
|
+
def get_tenant_config(self) -> Optional[Dict[str, Any]]:
|
|
15
|
+
"""Fetch tenant config (auth methods, branding, bot protection)."""
|
|
16
|
+
try:
|
|
17
|
+
r = requests.get(f"{self.base_url}/api/v1/accounts/config", params={"pk": self.publishable_key})
|
|
18
|
+
return r.json().get("data") if r.ok else None
|
|
19
|
+
except Exception:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def sign_in(self, email: str, password: str, account_id: str = "default") -> Dict[str, Any]:
|
|
23
|
+
"""Sign in with email + password. Returns accessToken, refreshToken, user."""
|
|
24
|
+
r = requests.post(f"{self.base_url}/api/v1/idm/auth/sign-in", json={
|
|
25
|
+
"email": email, "password": password, "accountId": account_id,
|
|
26
|
+
})
|
|
27
|
+
data = r.json()
|
|
28
|
+
if not data.get("success"):
|
|
29
|
+
raise Exception(data.get("error", "Sign in failed"))
|
|
30
|
+
return data["data"]
|
|
31
|
+
|
|
32
|
+
def sign_up(self, email: str, password: str, first_name: str = "", last_name: str = "",
|
|
33
|
+
account_id: str = "default") -> Dict[str, Any]:
|
|
34
|
+
"""Sign up a new user. Returns accessToken, refreshToken, user."""
|
|
35
|
+
r = requests.post(f"{self.base_url}/api/v1/idm/auth/sign-up", json={
|
|
36
|
+
"email": email, "password": password,
|
|
37
|
+
"firstName": first_name, "lastName": last_name, "accountId": account_id,
|
|
38
|
+
})
|
|
39
|
+
data = r.json()
|
|
40
|
+
if not data.get("success"):
|
|
41
|
+
raise Exception(data.get("error", "Sign up failed"))
|
|
42
|
+
return data["data"]
|
|
43
|
+
|
|
44
|
+
def refresh_token(self, refresh_token: str) -> Optional[Dict[str, Any]]:
|
|
45
|
+
"""Refresh an access token."""
|
|
46
|
+
try:
|
|
47
|
+
r = requests.post(f"{self.base_url}/api/v1/idm/auth/refresh", json={
|
|
48
|
+
"refreshToken": refresh_token,
|
|
49
|
+
})
|
|
50
|
+
return r.json().get("data") if r.ok else None
|
|
51
|
+
except Exception:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def get_me(self, access_token: str) -> Optional[Dict[str, Any]]:
|
|
55
|
+
"""Get current user profile."""
|
|
56
|
+
try:
|
|
57
|
+
r = requests.get(f"{self.base_url}/api/v1/idm/auth/me", headers={
|
|
58
|
+
"Authorization": f"Bearer {access_token}",
|
|
59
|
+
})
|
|
60
|
+
return r.json().get("data", {}).get("user") if r.ok else None
|
|
61
|
+
except Exception:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def sign_out(self, access_token: str) -> None:
|
|
65
|
+
"""Sign out — invalidate session."""
|
|
66
|
+
try:
|
|
67
|
+
requests.post(f"{self.base_url}/api/v1/idm/auth/sign-out", headers={
|
|
68
|
+
"Authorization": f"Bearer {access_token}",
|
|
69
|
+
})
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Thread-local security context for the authenticated user."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SecurityContext:
|
|
10
|
+
"""Holds the authenticated user's identity and permissions."""
|
|
11
|
+
user_id: str = ""
|
|
12
|
+
account_id: str = ""
|
|
13
|
+
email: str = ""
|
|
14
|
+
session_id: str = ""
|
|
15
|
+
permissions: List[str] = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_context = threading.local()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_context(ctx: SecurityContext) -> None:
|
|
22
|
+
"""Set the security context for the current thread."""
|
|
23
|
+
_context.kaappu = ctx
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_context() -> Optional[SecurityContext]:
|
|
27
|
+
"""Get the security context for the current thread."""
|
|
28
|
+
return getattr(_context, "kaappu", None)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clear_context() -> None:
|
|
32
|
+
"""Clear the security context for the current thread."""
|
|
33
|
+
_context.kaappu = None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for Flask and FastAPI route protection.
|
|
3
|
+
|
|
4
|
+
Flask usage:
|
|
5
|
+
@app.route("/api/users")
|
|
6
|
+
@require_permission("users:read")
|
|
7
|
+
def list_users():
|
|
8
|
+
...
|
|
9
|
+
|
|
10
|
+
FastAPI usage:
|
|
11
|
+
@app.get("/api/users")
|
|
12
|
+
async def list_users(ctx: SecurityContext = Depends(get_kaappu_context)):
|
|
13
|
+
...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from functools import wraps
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
from kaappu.context import get_context
|
|
20
|
+
from kaappu.permissions import check_permission
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_permission(permission: str) -> Callable:
|
|
24
|
+
"""
|
|
25
|
+
Decorator that checks the current SecurityContext for the required permission.
|
|
26
|
+
Returns 403 if the permission check fails.
|
|
27
|
+
Works with Flask routes.
|
|
28
|
+
"""
|
|
29
|
+
def decorator(f: Callable) -> Callable:
|
|
30
|
+
@wraps(f)
|
|
31
|
+
def wrapper(*args, **kwargs):
|
|
32
|
+
ctx = get_context()
|
|
33
|
+
if ctx is None or not check_permission(ctx.permissions, permission):
|
|
34
|
+
# Flask import deferred to avoid hard dependency
|
|
35
|
+
try:
|
|
36
|
+
from flask import jsonify
|
|
37
|
+
return jsonify({
|
|
38
|
+
"error": f"Forbidden: Requires {permission} permission",
|
|
39
|
+
"code": "forbidden",
|
|
40
|
+
}), 403
|
|
41
|
+
except ImportError:
|
|
42
|
+
raise PermissionError(f"Requires {permission} permission")
|
|
43
|
+
return f(*args, **kwargs)
|
|
44
|
+
return wrapper
|
|
45
|
+
return decorator
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework-agnostic permission checking with wildcard support.
|
|
3
|
+
Works with Flask, FastAPI, Django, or any Python service.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_permission(user_permissions: Optional[List[str]], required: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if the user's permissions satisfy the required permission.
|
|
12
|
+
|
|
13
|
+
Supports:
|
|
14
|
+
- Exact match: 'users:read' satisfies 'users:read'
|
|
15
|
+
- Super wildcard: '*' satisfies any permission
|
|
16
|
+
- Resource wildcard: 'users:*' satisfies 'users:read', 'users:delete', etc.
|
|
17
|
+
"""
|
|
18
|
+
if not required:
|
|
19
|
+
return True
|
|
20
|
+
if not user_permissions:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
resource = required.split(":")[0]
|
|
24
|
+
return any(
|
|
25
|
+
p == "*" or p == required or p == f"{resource}:*"
|
|
26
|
+
for p in user_permissions
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_all_permissions(user_permissions: Optional[List[str]], required: List[str]) -> bool:
|
|
31
|
+
"""Check if the user has ALL required permissions."""
|
|
32
|
+
return all(check_permission(user_permissions, r) for r in required)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_any_permission(user_permissions: Optional[List[str]], required: List[str]) -> bool:
|
|
36
|
+
"""Check if the user has ANY of the required permissions."""
|
|
37
|
+
return any(check_permission(user_permissions, r) for r in required)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kaappu-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JWT authentication and permission-based authorization for Python services
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: PyJWT[crypto]>=2.8.0
|
|
10
|
+
Requires-Dist: requests>=2.31.0
|
|
11
|
+
Provides-Extra: flask
|
|
12
|
+
Requires-Dist: Flask>=2.3.0; extra == "flask"
|
|
13
|
+
Provides-Extra: fastapi
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# kaappu-sdk
|
|
20
|
+
|
|
21
|
+
JWT authentication and permission-based authorization for Python services. Works with Flask, FastAPI, or any Python HTTP framework.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install kaappu-sdk
|
|
27
|
+
|
|
28
|
+
# With Flask support
|
|
29
|
+
pip install kaappu-sdk[flask]
|
|
30
|
+
|
|
31
|
+
# With FastAPI support
|
|
32
|
+
pip install kaappu-sdk[fastapi]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Permission Checking
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from kaappu import check_permission, check_all_permissions, check_any_permission
|
|
41
|
+
|
|
42
|
+
user_perms = ["users:read", "roles:read", "gateway:*"]
|
|
43
|
+
|
|
44
|
+
check_permission(user_perms, "users:read") # True
|
|
45
|
+
check_permission(user_perms, "users:delete") # False
|
|
46
|
+
check_permission(user_perms, "gateway:view") # True (wildcard)
|
|
47
|
+
check_all_permissions(user_perms, ["users:read", "roles:read"]) # True
|
|
48
|
+
check_any_permission(user_perms, ["users:delete", "roles:read"]) # True
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Wildcard support:
|
|
52
|
+
- `*` -- super wildcard, matches any permission
|
|
53
|
+
- `resource:*` -- matches any action on that resource
|
|
54
|
+
|
|
55
|
+
### Flask Route Protection
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from flask import Flask
|
|
59
|
+
from kaappu.decorators import require_permission
|
|
60
|
+
|
|
61
|
+
app = Flask(__name__)
|
|
62
|
+
|
|
63
|
+
@app.route("/api/users")
|
|
64
|
+
@require_permission("users:read")
|
|
65
|
+
def list_users():
|
|
66
|
+
return {"users": []}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Security Context
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from kaappu import SecurityContext
|
|
73
|
+
from kaappu.context import set_context, get_context
|
|
74
|
+
|
|
75
|
+
# Set context (typically in middleware)
|
|
76
|
+
ctx = SecurityContext(
|
|
77
|
+
user_id="u_123",
|
|
78
|
+
account_id="acc_456",
|
|
79
|
+
email="user@example.com",
|
|
80
|
+
permissions=["users:read", "roles:read"],
|
|
81
|
+
)
|
|
82
|
+
set_context(ctx)
|
|
83
|
+
|
|
84
|
+
# Read context anywhere in the same thread
|
|
85
|
+
current = get_context()
|
|
86
|
+
print(current.email)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### API Client
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from kaappu import KaappuClient
|
|
93
|
+
|
|
94
|
+
client = KaappuClient(
|
|
95
|
+
base_url="https://your-kaappu-instance",
|
|
96
|
+
publishable_key="pk_live_...",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Sign in
|
|
100
|
+
result = client.sign_in("user@example.com", "password")
|
|
101
|
+
access_token = result["accessToken"]
|
|
102
|
+
|
|
103
|
+
# Get current user
|
|
104
|
+
user = client.get_me(access_token)
|
|
105
|
+
|
|
106
|
+
# Refresh token
|
|
107
|
+
new_tokens = client.refresh_token(result["refreshToken"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
| Function / Class | Description |
|
|
113
|
+
|------------------|-------------|
|
|
114
|
+
| `check_permission(perms, required)` | Check a single permission with wildcard support |
|
|
115
|
+
| `check_all_permissions(perms, required)` | Check that ALL permissions are satisfied |
|
|
116
|
+
| `check_any_permission(perms, required)` | Check that ANY permission is satisfied |
|
|
117
|
+
| `SecurityContext` | Dataclass holding user identity and permissions |
|
|
118
|
+
| `set_context(ctx)` / `get_context()` | Thread-local security context storage |
|
|
119
|
+
| `require_permission(perm)` | Flask route decorator for permission enforcement |
|
|
120
|
+
| `KaappuClient` | API client for sign-in, sign-up, refresh, and user fetch |
|
|
121
|
+
|
|
122
|
+
## Requirements
|
|
123
|
+
|
|
124
|
+
- Python 3.9+
|
|
125
|
+
- PyJWT 2.8+
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
kaappu/__init__.py
|
|
5
|
+
kaappu/client.py
|
|
6
|
+
kaappu/context.py
|
|
7
|
+
kaappu/decorators.py
|
|
8
|
+
kaappu/permissions.py
|
|
9
|
+
kaappu_sdk.egg-info/PKG-INFO
|
|
10
|
+
kaappu_sdk.egg-info/SOURCES.txt
|
|
11
|
+
kaappu_sdk.egg-info/dependency_links.txt
|
|
12
|
+
kaappu_sdk.egg-info/requires.txt
|
|
13
|
+
kaappu_sdk.egg-info/top_level.txt
|
|
14
|
+
tests/test_permissions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kaappu
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kaappu-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "JWT authentication and permission-based authorization for Python services"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"PyJWT[crypto]>=2.8.0",
|
|
14
|
+
"requests>=2.31.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
flask = ["Flask>=2.3.0"]
|
|
19
|
+
fastapi = ["fastapi>=0.100.0"]
|
|
20
|
+
dev = ["pytest>=7.0.0"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["kaappu*"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tests for the permission checking module."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from kaappu.permissions import check_permission, check_all_permissions, check_any_permission
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestCheckPermission:
|
|
8
|
+
def test_exact_match(self):
|
|
9
|
+
assert check_permission(["users:read"], "users:read") is True
|
|
10
|
+
|
|
11
|
+
def test_no_match(self):
|
|
12
|
+
assert check_permission(["users:read"], "users:delete") is False
|
|
13
|
+
|
|
14
|
+
def test_super_wildcard(self):
|
|
15
|
+
assert check_permission(["*"], "anything:here") is True
|
|
16
|
+
|
|
17
|
+
def test_resource_wildcard(self):
|
|
18
|
+
assert check_permission(["users:*"], "users:delete") is True
|
|
19
|
+
|
|
20
|
+
def test_resource_wildcard_no_cross(self):
|
|
21
|
+
assert check_permission(["users:*"], "roles:read") is False
|
|
22
|
+
|
|
23
|
+
def test_empty_perms(self):
|
|
24
|
+
assert check_permission([], "users:read") is False
|
|
25
|
+
|
|
26
|
+
def test_none_perms(self):
|
|
27
|
+
assert check_permission(None, "users:read") is False
|
|
28
|
+
|
|
29
|
+
def test_empty_required(self):
|
|
30
|
+
assert check_permission([], "") is True
|
|
31
|
+
|
|
32
|
+
def test_owner_role(self):
|
|
33
|
+
assert check_permission(["*"], "gateway_instances:manage") is True
|
|
34
|
+
|
|
35
|
+
def test_admin_wildcards(self):
|
|
36
|
+
perms = ["users:*", "roles:*", "gateway:*"]
|
|
37
|
+
assert check_permission(perms, "users:delete") is True
|
|
38
|
+
assert check_permission(perms, "gateway:view") is True
|
|
39
|
+
assert check_permission(perms, "audit:read") is False
|
|
40
|
+
|
|
41
|
+
def test_viewer_denied_write(self):
|
|
42
|
+
perms = ["users:read", "roles:read"]
|
|
43
|
+
assert check_permission(perms, "users:read") is True
|
|
44
|
+
assert check_permission(perms, "users:delete") is False
|
|
45
|
+
|
|
46
|
+
def test_member_chat(self):
|
|
47
|
+
perms = ["governance_chat:use", "gateway_chat:use", "users:read"]
|
|
48
|
+
assert check_permission(perms, "governance_chat:use") is True
|
|
49
|
+
assert check_permission(perms, "gateway_instances:manage") is False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestCheckAllPermissions:
|
|
53
|
+
def test_all_present(self):
|
|
54
|
+
assert check_all_permissions(["users:read", "roles:read"], ["users:read", "roles:read"]) is True
|
|
55
|
+
|
|
56
|
+
def test_one_missing(self):
|
|
57
|
+
assert check_all_permissions(["users:read"], ["users:read", "users:delete"]) is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestCheckAnyPermission:
|
|
61
|
+
def test_one_matches(self):
|
|
62
|
+
assert check_any_permission(["users:read"], ["users:delete", "users:read"]) is True
|
|
63
|
+
|
|
64
|
+
def test_none_match(self):
|
|
65
|
+
assert check_any_permission(["users:read"], ["roles:read", "groups:read"]) is False
|