Coverage for src/lexigram/graphql/security/permissions.py: 100%
75 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""GraphQL permission classes for field-level authorization.
3This module provides permission classes that can be used to control
4access to GraphQL fields, mutations, and subscriptions at the field level.
5"""
7from __future__ import annotations
9import abc
10from typing import TYPE_CHECKING, Any
12from lexigram.di.decorators import singleton
14if TYPE_CHECKING:
15 from strawberry import Info
17 from lexigram.graphql.core.context import GraphQLContext
20class AbstractPermission(abc.ABC):
21 """Base class for GraphQL permissions.
23 Permission classes control access to GraphQL fields. They are checked
24 before field resolution and can raise exceptions or return False to
25 deny access.
27 Attributes:
28 message: Error message when permission is denied.
30 Example:
31 ```python
32 class IsAuthenticated(AbstractPermission):
33 message = "Authentication required"
35 async def has_permission(self, source, info, **kwargs):
36 return info.context.user is not None
38 @strawberry.type
39 class User:
40 @field(permission_classes=[IsAuthenticated])
41 async def email(self) -> str:
42 return self._email
43 ```
44 """
46 message: str = "Permission denied"
48 @abc.abstractmethod
49 async def has_permission(
50 self,
51 source: Any,
52 info: Info,
53 **kwargs: Any,
54 ) -> bool:
55 """Check if the permission is granted.
57 Args:
58 source: The source object (parent resolver result).
59 info: GraphQL resolver info containing context.
60 **kwargs: Additional arguments passed to the field.
62 Returns:
63 True if permission is granted, False otherwise.
65 Raises:
66 Exception: If permission is denied with a custom error.
67 """
68 ...
71@singleton
72class IsAuthenticated(AbstractPermission):
73 """Permission that requires user authentication."""
75 message = "Authentication required"
77 async def has_permission(
78 self,
79 source: Any,
80 info: Info[Any, Any],
81 **kwargs: Any,
82 ) -> bool:
83 """Check if user is authenticated."""
84 context: GraphQLContext = info.context
85 return context.user is not None
88@singleton
89class IsAdmin(AbstractPermission):
90 """Permission that requires admin role."""
92 message = "Admin access required"
94 async def has_permission(
95 self,
96 source: Any,
97 info: Info[Any, Any],
98 **kwargs: Any,
99 ) -> bool:
100 """Check if user has admin role."""
101 context: GraphQLContext = info.context
102 if not context.user:
103 return False
105 # Check for admin role - adjust based on your user model
106 user_roles = getattr(context.user, "roles", [])
107 return "admin" in user_roles or "superuser" in user_roles
110@singleton
111class IsOwner(AbstractPermission):
112 """Permission that requires ownership of the resource."""
114 message = "Access denied: not the owner"
116 async def has_permission(
117 self,
118 source: Any,
119 info: Info[Any, Any],
120 **kwargs: Any,
121 ) -> bool:
122 """Check if user owns the resource."""
123 context: GraphQLContext = info.context
124 if not context.user:
125 return False
127 # Check if source has user_id or owner_id field
128 source_user_id = None
129 user_id_attr = getattr(source, "user_id", None)
130 if isinstance(user_id_attr, (str, int)):
131 source_user_id = user_id_attr
132 else:
133 owner_id_attr = getattr(source, "owner_id", None)
134 if isinstance(owner_id_attr, (str, int)):
135 source_user_id = owner_id_attr
137 if source_user_id is None:
138 return False
140 # Compare with current user ID
141 current_user_id = None
142 user_id_attr = getattr(context.user, "user_id", None)
143 if isinstance(user_id_attr, (str, int)):
144 current_user_id = user_id_attr
145 else:
146 id_attr = getattr(context.user, "id", None)
147 if isinstance(id_attr, (str, int)):
148 current_user_id = id_attr
150 if current_user_id is None:
151 return False
153 return source_user_id == current_user_id
156@singleton
157class IsOwnerOrAdmin(AbstractPermission):
158 """Permission that requires ownership or admin role."""
160 message = "Access denied: not the owner or admin"
162 async def has_permission(
163 self,
164 source: Any,
165 info: Info[Any, Any],
166 **kwargs: Any,
167 ) -> bool:
168 """Check if user owns the resource or is admin."""
169 # First check ownership
170 owner_perm = IsOwner()
171 if await owner_perm.has_permission(source, info, **kwargs):
172 return True
174 # Then check admin
175 admin_perm = IsAdmin()
176 return await admin_perm.has_permission(source, info, **kwargs)
179@singleton
180class AllowAny(AbstractPermission):
181 """Permission that allows any access (no restrictions)."""
183 message = "This should never be shown"
185 async def has_permission(
186 self,
187 source: Any,
188 info: Info[Any, Any],
189 **kwargs: Any,
190 ) -> bool:
191 """Always allow access."""
192 return True
195@singleton
196class DenyAll(AbstractPermission):
197 """Permission that denies all access."""
199 message = "Access denied"
201 async def has_permission(
202 self,
203 source: Any,
204 info: Info[Any, Any],
205 **kwargs: Any,
206 ) -> bool:
207 """Always deny access."""
208 return False
211# Convenience instances for common permissions
212# Note: These use fallback instances. For DI-integrated permissions,
213# resolve them at request time using the container.
214is_authenticated = IsAuthenticated()
215is_admin = IsAdmin()
216is_owner = IsOwner()
217is_owner_or_admin = IsOwnerOrAdmin()
218allow_any = AllowAny()
219deny_all = DenyAll()
222__all__ = [
223 "AbstractPermission",
224 "AllowAny",
225 "DenyAll",
226 "IsAdmin",
227 "IsAuthenticated",
228 "IsOwner",
229 "IsOwnerOrAdmin",
230 "allow_any",
231 "deny_all",
232 "is_admin",
233 "is_authenticated",
234 "is_owner",
235 "is_owner_or_admin",
236]