vs-security 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.
- vs_security-0.1.0/PKG-INFO +29 -0
- vs_security-0.1.0/pyproject.toml +53 -0
- vs_security-0.1.0/setup.cfg +4 -0
- vs_security-0.1.0/vs_security/__init__.py +37 -0
- vs_security-0.1.0/vs_security/auth/__init__.py +13 -0
- vs_security-0.1.0/vs_security/auth/vs_auth_manager.py +25 -0
- vs_security-0.1.0/vs_security/auth/vs_auth_provider.py +10 -0
- vs_security-0.1.0/vs_security/auth/vs_jwt_provider.py +100 -0
- vs_security-0.1.0/vs_security/auth/vs_token_store.py +18 -0
- vs_security-0.1.0/vs_security/auth/vs_username_password_provider.py +39 -0
- vs_security-0.1.0/vs_security/error/__init__.py +15 -0
- vs_security-0.1.0/vs_security/error/vs_auth_error.py +25 -0
- vs_security-0.1.0/vs_security/guard/__init__.py +6 -0
- vs_security-0.1.0/vs_security/guard/vs_security.py +42 -0
- vs_security-0.1.0/vs_security/guard/vs_security_factory.py +43 -0
- vs_security-0.1.0/vs_security/schema/__init__.py +12 -0
- vs_security-0.1.0/vs_security/schema/vs_auth_context.py +23 -0
- vs_security-0.1.0/vs_security/schema/vs_auth_metadata.py +5 -0
- vs_security-0.1.0/vs_security/schema/vs_credentials.py +10 -0
- vs_security-0.1.0/vs_security/schema/vs_token_pair.py +6 -0
- vs_security-0.1.0/vs_security.egg-info/PKG-INFO +29 -0
- vs_security-0.1.0/vs_security.egg-info/SOURCES.txt +23 -0
- vs_security-0.1.0/vs_security.egg-info/dependency_links.txt +1 -0
- vs_security-0.1.0/vs_security.egg-info/requires.txt +13 -0
- vs_security-0.1.0/vs_security.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vs-security
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security library for Viveka Sutra — authentication, JWT, and authorization
|
|
5
|
+
Project-URL: Homepage, https://vivekasutra.com/
|
|
6
|
+
Project-URL: Source, https://github.com/vivekasutra/viveka-mula
|
|
7
|
+
Keywords: security,auth,jwt,authentication,viveka,vs
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: pyjwt>=2.8
|
|
21
|
+
Requires-Dist: bcrypt>=4.0
|
|
22
|
+
Requires-Dist: vs-common
|
|
23
|
+
Provides-Extra: fastapi
|
|
24
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vs-security"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Security library for Viveka Sutra — authentication, JWT, and authorization"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { file = "LICENSE.txt" }
|
|
12
|
+
keywords = ["security", "auth", "jwt", "authentication", "viveka", "vs"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: Other/Proprietary License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Framework :: AsyncIO",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pydantic>=2.0",
|
|
27
|
+
"pyjwt>=2.8",
|
|
28
|
+
"bcrypt>=4.0",
|
|
29
|
+
"vs-common",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
fastapi = [
|
|
34
|
+
"fastapi>=0.110",
|
|
35
|
+
]
|
|
36
|
+
dev = [
|
|
37
|
+
"build",
|
|
38
|
+
"twine",
|
|
39
|
+
"pytest>=8.0",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://vivekasutra.com/"
|
|
45
|
+
Source = "https://github.com/vivekasutra/viveka-mula"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["."]
|
|
49
|
+
include = ["vs_security*"]
|
|
50
|
+
exclude = [".venv*"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
vs_security = ["py.typed"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from vs_security.schema.vs_auth_metadata import VsAuthMetadata
|
|
2
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
3
|
+
from vs_security.schema.vs_credentials import VsCredentials, VsUsernamePasswordCredentials
|
|
4
|
+
from vs_security.schema.vs_token_pair import VsTokenPair
|
|
5
|
+
from vs_security.auth.vs_token_store import VsTokenStore
|
|
6
|
+
from vs_security.auth.vs_auth_provider import VsAuthProvider
|
|
7
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
8
|
+
from vs_security.auth.vs_auth_manager import VsAuthManager
|
|
9
|
+
from vs_security.auth.vs_username_password_provider import VsUsernamePasswordAuthProvider
|
|
10
|
+
from vs_security.guard.vs_security import VsSecurity, get_auth_context
|
|
11
|
+
from vs_security.error.vs_auth_error import (
|
|
12
|
+
VsAuthenticationError,
|
|
13
|
+
VsTokenExpiredError,
|
|
14
|
+
VsInsufficientRolesError,
|
|
15
|
+
VsInvalidCredentialsError,
|
|
16
|
+
VsTokenRevokedError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"VsAuthMetadata",
|
|
21
|
+
"VsAuthContext",
|
|
22
|
+
"VsCredentials",
|
|
23
|
+
"VsUsernamePasswordCredentials",
|
|
24
|
+
"VsTokenPair",
|
|
25
|
+
"VsTokenStore",
|
|
26
|
+
"VsAuthProvider",
|
|
27
|
+
"VsJWTProvider",
|
|
28
|
+
"VsAuthManager",
|
|
29
|
+
"VsUsernamePasswordAuthProvider",
|
|
30
|
+
"VsSecurity",
|
|
31
|
+
"get_auth_context",
|
|
32
|
+
"VsAuthenticationError",
|
|
33
|
+
"VsTokenExpiredError",
|
|
34
|
+
"VsInsufficientRolesError",
|
|
35
|
+
"VsInvalidCredentialsError",
|
|
36
|
+
"VsTokenRevokedError",
|
|
37
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from vs_security.auth.vs_auth_provider import VsAuthProvider
|
|
2
|
+
from vs_security.auth.vs_token_store import VsTokenStore
|
|
3
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
4
|
+
from vs_security.auth.vs_auth_manager import VsAuthManager
|
|
5
|
+
from vs_security.auth.vs_username_password_provider import VsUsernamePasswordAuthProvider
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"VsAuthProvider",
|
|
9
|
+
"VsTokenStore",
|
|
10
|
+
"VsJWTProvider",
|
|
11
|
+
"VsAuthManager",
|
|
12
|
+
"VsUsernamePasswordAuthProvider",
|
|
13
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from vs_common.log.vs_log_manager import VsLogManager
|
|
2
|
+
from vs_security.auth.vs_auth_provider import VsAuthProvider
|
|
3
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
4
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
5
|
+
from vs_security.schema.vs_credentials import VsCredentials
|
|
6
|
+
from vs_security.schema.vs_token_pair import VsTokenPair
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VsAuthManager:
|
|
10
|
+
|
|
11
|
+
def __init__(self, jwt_provider: VsJWTProvider):
|
|
12
|
+
self._jwt_provider = jwt_provider
|
|
13
|
+
self._providers: dict[str, VsAuthProvider] = {}
|
|
14
|
+
self._logger = VsLogManager.get_instance(self.__class__.__name__)
|
|
15
|
+
|
|
16
|
+
def register(self, name: str, provider: VsAuthProvider) -> None:
|
|
17
|
+
self._providers[name] = provider
|
|
18
|
+
self._logger.info(f"Registered auth provider: {name}")
|
|
19
|
+
|
|
20
|
+
async def authenticate(self, provider_name: str, credentials: VsCredentials) -> VsTokenPair:
|
|
21
|
+
provider = self._providers.get(provider_name)
|
|
22
|
+
if not provider:
|
|
23
|
+
raise ValueError(f"No auth provider registered with name: {provider_name}")
|
|
24
|
+
context: VsAuthContext = await provider.authenticate(credentials)
|
|
25
|
+
return await self._jwt_provider.generate_token(context)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
4
|
+
from vs_security.schema.vs_credentials import VsCredentials
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VsAuthProvider(ABC):
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
async def authenticate(self, credentials: VsCredentials) -> VsAuthContext: ...
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
import jwt
|
|
6
|
+
|
|
7
|
+
from vs_common.config.vs_base_config import VsBaseConfig
|
|
8
|
+
from vs_common.log.vs_log_manager import VsLogManager
|
|
9
|
+
from vs_security.auth.vs_token_store import VsTokenStore
|
|
10
|
+
from vs_security.error.vs_auth_error import VsTokenExpiredError, VsAuthenticationError, VsTokenRevokedError
|
|
11
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
12
|
+
from vs_security.schema.vs_token_pair import VsTokenPair
|
|
13
|
+
|
|
14
|
+
_DEFAULT_ALGORITHM = "HS256"
|
|
15
|
+
_DEFAULT_ACCESS_EXPIRY_MINUTES = 15
|
|
16
|
+
_DEFAULT_REFRESH_EXPIRY_DAYS = 7
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VsJWTProvider:
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: VsBaseConfig, token_store: Optional[VsTokenStore] = None):
|
|
22
|
+
self._logger = VsLogManager.get_instance(self.__class__.__name__)
|
|
23
|
+
self._token_store = token_store
|
|
24
|
+
self._secret = config.get("auth.secret_key")
|
|
25
|
+
self._algorithm = config.get("auth.algorithm", default=_DEFAULT_ALGORITHM)
|
|
26
|
+
self._access_expiry_minutes = config.get(
|
|
27
|
+
"auth.access_expiry_minutes", default=_DEFAULT_ACCESS_EXPIRY_MINUTES, data_type=int
|
|
28
|
+
)
|
|
29
|
+
self._refresh_expiry_days = config.get(
|
|
30
|
+
"auth.refresh_expiry_days", default=_DEFAULT_REFRESH_EXPIRY_DAYS, data_type=int
|
|
31
|
+
)
|
|
32
|
+
if not self._secret:
|
|
33
|
+
raise ValueError("auth.secret_key must be set in config.ini")
|
|
34
|
+
|
|
35
|
+
async def generate_token(self, context: VsAuthContext) -> VsTokenPair:
|
|
36
|
+
access_token = self._mint(context, self._access_expiry_minutes * 60, "access")
|
|
37
|
+
refresh_token = self._mint(context, self._refresh_expiry_days * 86400, "refresh")
|
|
38
|
+
if self._token_store:
|
|
39
|
+
await self._token_store.save(context.user_id, refresh_token)
|
|
40
|
+
self._logger.debug(f"Generated token pair for user {context.user_id}")
|
|
41
|
+
return VsTokenPair(access_token=access_token, refresh_token=refresh_token)
|
|
42
|
+
|
|
43
|
+
async def verify_token(self, token: str) -> VsAuthContext:
|
|
44
|
+
payload = self._decode(token, expected_type="access")
|
|
45
|
+
return self._payload_to_context(payload)
|
|
46
|
+
|
|
47
|
+
async def refresh_token(self, refresh_token: str) -> VsTokenPair:
|
|
48
|
+
payload = self._decode(refresh_token, expected_type="refresh")
|
|
49
|
+
user_id = UUID(payload["sub"])
|
|
50
|
+
|
|
51
|
+
if self._token_store:
|
|
52
|
+
stored = await self._token_store.get_all(user_id)
|
|
53
|
+
if refresh_token not in stored:
|
|
54
|
+
raise VsTokenRevokedError()
|
|
55
|
+
await self._token_store.delete(user_id, refresh_token)
|
|
56
|
+
|
|
57
|
+
context = self._payload_to_context(payload)
|
|
58
|
+
return await self.generate_token(context)
|
|
59
|
+
|
|
60
|
+
async def revoke_token(self, user_id: UUID, refresh_token: str) -> None:
|
|
61
|
+
if not self._token_store:
|
|
62
|
+
raise NotImplementedError("No VsTokenStore configured")
|
|
63
|
+
await self._token_store.delete(user_id, refresh_token)
|
|
64
|
+
|
|
65
|
+
async def revoke_all_tokens(self, user_id: UUID) -> None:
|
|
66
|
+
if not self._token_store:
|
|
67
|
+
raise NotImplementedError("No VsTokenStore configured")
|
|
68
|
+
await self._token_store.delete_all(user_id)
|
|
69
|
+
|
|
70
|
+
def _mint(self, context: VsAuthContext, expiry_seconds: int, token_type: str) -> str:
|
|
71
|
+
now = datetime.now(timezone.utc)
|
|
72
|
+
payload = {
|
|
73
|
+
"sub": str(context.user_id),
|
|
74
|
+
"username": context.username,
|
|
75
|
+
"roles": context.roles,
|
|
76
|
+
"provider": context.provider,
|
|
77
|
+
"type": token_type,
|
|
78
|
+
"iat": now,
|
|
79
|
+
"exp": now + timedelta(seconds=expiry_seconds),
|
|
80
|
+
}
|
|
81
|
+
return jwt.encode(payload, self._secret, algorithm=self._algorithm)
|
|
82
|
+
|
|
83
|
+
def _decode(self, token: str, expected_type: str) -> dict:
|
|
84
|
+
try:
|
|
85
|
+
payload = jwt.decode(token, self._secret, algorithms=[self._algorithm])
|
|
86
|
+
if payload.get("type") != expected_type:
|
|
87
|
+
raise VsAuthenticationError(f"Expected {expected_type} token")
|
|
88
|
+
return payload
|
|
89
|
+
except jwt.ExpiredSignatureError:
|
|
90
|
+
raise VsTokenExpiredError()
|
|
91
|
+
except jwt.InvalidTokenError as e:
|
|
92
|
+
raise VsAuthenticationError(f"Invalid token: {e}")
|
|
93
|
+
|
|
94
|
+
def _payload_to_context(self, payload: dict) -> VsAuthContext:
|
|
95
|
+
return VsAuthContext(
|
|
96
|
+
user_id=UUID(payload["sub"]),
|
|
97
|
+
username=payload["username"],
|
|
98
|
+
roles=payload.get("roles", []),
|
|
99
|
+
provider=payload.get("provider", ""),
|
|
100
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VsTokenStore(ABC):
|
|
7
|
+
|
|
8
|
+
@abstractmethod
|
|
9
|
+
async def save(self, user_id: UUID, refresh_token: str) -> None: ...
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def get_all(self, user_id: UUID) -> List[str]: ...
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
async def delete(self, user_id: UUID, refresh_token: str) -> None: ...
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
async def delete_all(self, user_id: UUID) -> None: ...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Awaitable, Callable, Tuple
|
|
2
|
+
|
|
3
|
+
import bcrypt
|
|
4
|
+
|
|
5
|
+
from vs_common.log.vs_log_manager import VsLogManager
|
|
6
|
+
from vs_security.auth.vs_auth_provider import VsAuthProvider
|
|
7
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
8
|
+
from vs_security.error.vs_auth_error import VsInvalidCredentialsError
|
|
9
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
10
|
+
from vs_security.schema.vs_credentials import VsCredentials, VsUsernamePasswordCredentials
|
|
11
|
+
from vs_security.schema.vs_token_pair import VsTokenPair
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VsUsernamePasswordAuthProvider(VsAuthProvider):
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
jwt_provider: VsJWTProvider,
|
|
19
|
+
user_loader: Callable[[str], Awaitable[Tuple[VsAuthContext, str]]],
|
|
20
|
+
):
|
|
21
|
+
self._jwt_provider = jwt_provider
|
|
22
|
+
self._user_loader = user_loader
|
|
23
|
+
self._logger = VsLogManager.get_instance(self.__class__.__name__)
|
|
24
|
+
|
|
25
|
+
async def authenticate(self, credentials: VsCredentials) -> VsAuthContext:
|
|
26
|
+
if not isinstance(credentials, VsUsernamePasswordCredentials):
|
|
27
|
+
raise TypeError("Expected VsUsernamePasswordCredentials")
|
|
28
|
+
|
|
29
|
+
result = await self._user_loader(credentials.username)
|
|
30
|
+
if result is None:
|
|
31
|
+
raise VsInvalidCredentialsError()
|
|
32
|
+
|
|
33
|
+
context, hashed_password = result
|
|
34
|
+
|
|
35
|
+
if not bcrypt.checkpw(credentials.password.encode(), hashed_password.encode()):
|
|
36
|
+
raise VsInvalidCredentialsError()
|
|
37
|
+
|
|
38
|
+
self._logger.debug(f"Authenticated user {credentials.username}")
|
|
39
|
+
return context
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from vs_security.error.vs_auth_error import (
|
|
2
|
+
VsAuthenticationError,
|
|
3
|
+
VsTokenExpiredError,
|
|
4
|
+
VsInsufficientRolesError,
|
|
5
|
+
VsInvalidCredentialsError,
|
|
6
|
+
VsTokenRevokedError,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"VsAuthenticationError",
|
|
11
|
+
"VsTokenExpiredError",
|
|
12
|
+
"VsInsufficientRolesError",
|
|
13
|
+
"VsInvalidCredentialsError",
|
|
14
|
+
"VsTokenRevokedError",
|
|
15
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class VsAuthenticationError(Exception):
|
|
2
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
3
|
+
super().__init__(message)
|
|
4
|
+
self.message = message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VsTokenExpiredError(VsAuthenticationError):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
super().__init__("Token has expired")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VsInsufficientRolesError(VsAuthenticationError):
|
|
13
|
+
def __init__(self, required: list):
|
|
14
|
+
super().__init__(f"Required roles not present: {required}")
|
|
15
|
+
self.required = required
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VsInvalidCredentialsError(VsAuthenticationError):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__("Invalid username or password")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VsTokenRevokedError(VsAuthenticationError):
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__("Token has been revoked")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import Depends
|
|
5
|
+
from fastapi.exceptions import HTTPException
|
|
6
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
7
|
+
|
|
8
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
9
|
+
from vs_security.error.vs_auth_error import VsAuthenticationError, VsInsufficientRolesError
|
|
10
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
11
|
+
|
|
12
|
+
_bearer = HTTPBearer()
|
|
13
|
+
|
|
14
|
+
_auth_context_var: ContextVar[Optional[VsAuthContext]] = ContextVar("auth_context", default=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_auth_context() -> Optional[VsAuthContext]:
|
|
18
|
+
return _auth_context_var.get()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VsSecurity:
|
|
22
|
+
|
|
23
|
+
def __init__(self, jwt_provider: VsJWTProvider, roles: Optional[List[str]] = None):
|
|
24
|
+
self._jwt_provider = jwt_provider
|
|
25
|
+
self._roles = roles or []
|
|
26
|
+
|
|
27
|
+
async def __call__(
|
|
28
|
+
self, credentials: HTTPAuthorizationCredentials = Depends(_bearer)
|
|
29
|
+
) -> VsAuthContext:
|
|
30
|
+
try:
|
|
31
|
+
context = await self._jwt_provider.verify_token(credentials.credentials)
|
|
32
|
+
except VsAuthenticationError as e:
|
|
33
|
+
raise HTTPException(status_code=401, detail=str(e))
|
|
34
|
+
|
|
35
|
+
if self._roles and not context.has_any_role(*self._roles):
|
|
36
|
+
raise HTTPException(
|
|
37
|
+
status_code=403,
|
|
38
|
+
detail=f"Required roles: {self._roles}",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_auth_context_var.set(context)
|
|
42
|
+
return context
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from vs_security.guard.vs_security import VsSecurity
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class VsSecurityFactory:
|
|
7
|
+
|
|
8
|
+
_instance: Optional[VsSecurity] = None
|
|
9
|
+
_jwt_provider = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def init(cls, secret_key: str, algorithm: str = "HS256", roles: Optional[List[str]] = None) -> None:
|
|
13
|
+
from vs_security.auth.vs_jwt_provider import VsJWTProvider
|
|
14
|
+
|
|
15
|
+
class _InlineConfig:
|
|
16
|
+
def get(self, key, default=None, data_type=str):
|
|
17
|
+
if key == "auth.secret_key":
|
|
18
|
+
return secret_key
|
|
19
|
+
if key == "auth.algorithm":
|
|
20
|
+
return algorithm
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
def get_section(self, section):
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
cls._jwt_provider = VsJWTProvider(config=_InlineConfig())
|
|
27
|
+
cls._instance = VsSecurity(jwt_provider=cls._jwt_provider, roles=roles)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get(cls) -> VsSecurity:
|
|
31
|
+
if cls._instance is None:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
"VsSecurityFactory not initialized. Call VsSecurityFactory.init() at startup."
|
|
34
|
+
)
|
|
35
|
+
return cls._instance
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def with_roles(cls, roles: List[str]) -> VsSecurity:
|
|
39
|
+
if cls._jwt_provider is None:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"VsSecurityFactory not initialized. Call VsSecurityFactory.init() at startup."
|
|
42
|
+
)
|
|
43
|
+
return VsSecurity(jwt_provider=cls._jwt_provider, roles=roles)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from vs_security.schema.vs_auth_metadata import VsAuthMetadata
|
|
2
|
+
from vs_security.schema.vs_auth_context import VsAuthContext
|
|
3
|
+
from vs_security.schema.vs_credentials import VsCredentials, VsUsernamePasswordCredentials
|
|
4
|
+
from vs_security.schema.vs_token_pair import VsTokenPair
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"VsAuthMetadata",
|
|
8
|
+
"VsAuthContext",
|
|
9
|
+
"VsCredentials",
|
|
10
|
+
"VsUsernamePasswordCredentials",
|
|
11
|
+
"VsTokenPair",
|
|
12
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from vs_security.schema.vs_auth_metadata import VsAuthMetadata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VsAuthContext(BaseModel):
|
|
10
|
+
user_id: UUID
|
|
11
|
+
username: str
|
|
12
|
+
roles: List[str] = []
|
|
13
|
+
provider: str
|
|
14
|
+
metadata: VsAuthMetadata = VsAuthMetadata()
|
|
15
|
+
|
|
16
|
+
def has_role(self, role: str) -> bool:
|
|
17
|
+
return role in self.roles
|
|
18
|
+
|
|
19
|
+
def has_any_role(self, *roles: str) -> bool:
|
|
20
|
+
return any(r in self.roles for r in roles)
|
|
21
|
+
|
|
22
|
+
def has_all_roles(self, *roles: str) -> bool:
|
|
23
|
+
return all(r in self.roles for r in roles)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vs-security
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security library for Viveka Sutra — authentication, JWT, and authorization
|
|
5
|
+
Project-URL: Homepage, https://vivekasutra.com/
|
|
6
|
+
Project-URL: Source, https://github.com/vivekasutra/viveka-mula
|
|
7
|
+
Keywords: security,auth,jwt,authentication,viveka,vs
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: pyjwt>=2.8
|
|
21
|
+
Requires-Dist: bcrypt>=4.0
|
|
22
|
+
Requires-Dist: vs-common
|
|
23
|
+
Provides-Extra: fastapi
|
|
24
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
vs_security/__init__.py
|
|
3
|
+
vs_security.egg-info/PKG-INFO
|
|
4
|
+
vs_security.egg-info/SOURCES.txt
|
|
5
|
+
vs_security.egg-info/dependency_links.txt
|
|
6
|
+
vs_security.egg-info/requires.txt
|
|
7
|
+
vs_security.egg-info/top_level.txt
|
|
8
|
+
vs_security/auth/__init__.py
|
|
9
|
+
vs_security/auth/vs_auth_manager.py
|
|
10
|
+
vs_security/auth/vs_auth_provider.py
|
|
11
|
+
vs_security/auth/vs_jwt_provider.py
|
|
12
|
+
vs_security/auth/vs_token_store.py
|
|
13
|
+
vs_security/auth/vs_username_password_provider.py
|
|
14
|
+
vs_security/error/__init__.py
|
|
15
|
+
vs_security/error/vs_auth_error.py
|
|
16
|
+
vs_security/guard/__init__.py
|
|
17
|
+
vs_security/guard/vs_security.py
|
|
18
|
+
vs_security/guard/vs_security_factory.py
|
|
19
|
+
vs_security/schema/__init__.py
|
|
20
|
+
vs_security/schema/vs_auth_context.py
|
|
21
|
+
vs_security/schema/vs_auth_metadata.py
|
|
22
|
+
vs_security/schema/vs_credentials.py
|
|
23
|
+
vs_security/schema/vs_token_pair.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vs_security
|