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

1"""GraphQL permission classes for field-level authorization. 

2 

3This module provides permission classes that can be used to control 

4access to GraphQL fields, mutations, and subscriptions at the field level. 

5""" 

6 

7from __future__ import annotations 

8 

9import abc 

10from typing import TYPE_CHECKING, Any 

11 

12from lexigram.di.decorators import singleton 

13 

14if TYPE_CHECKING: 

15 from strawberry import Info 

16 

17 from lexigram.graphql.core.context import GraphQLContext 

18 

19 

20class AbstractPermission(abc.ABC): 

21 """Base class for GraphQL permissions. 

22 

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. 

26 

27 Attributes: 

28 message: Error message when permission is denied. 

29 

30 Example: 

31 ```python 

32 class IsAuthenticated(AbstractPermission): 

33 message = "Authentication required" 

34 

35 async def has_permission(self, source, info, **kwargs): 

36 return info.context.user is not None 

37 

38 @strawberry.type 

39 class User: 

40 @field(permission_classes=[IsAuthenticated]) 

41 async def email(self) -> str: 

42 return self._email 

43 ``` 

44 """ 

45 

46 message: str = "Permission denied" 

47 

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. 

56 

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. 

61 

62 Returns: 

63 True if permission is granted, False otherwise. 

64 

65 Raises: 

66 Exception: If permission is denied with a custom error. 

67 """ 

68 ... 

69 

70 

71@singleton 

72class IsAuthenticated(AbstractPermission): 

73 """Permission that requires user authentication.""" 

74 

75 message = "Authentication required" 

76 

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 

86 

87 

88@singleton 

89class IsAdmin(AbstractPermission): 

90 """Permission that requires admin role.""" 

91 

92 message = "Admin access required" 

93 

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 

104 

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 

108 

109 

110@singleton 

111class IsOwner(AbstractPermission): 

112 """Permission that requires ownership of the resource.""" 

113 

114 message = "Access denied: not the owner" 

115 

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 

126 

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 

136 

137 if source_user_id is None: 

138 return False 

139 

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 

149 

150 if current_user_id is None: 

151 return False 

152 

153 return source_user_id == current_user_id 

154 

155 

156@singleton 

157class IsOwnerOrAdmin(AbstractPermission): 

158 """Permission that requires ownership or admin role.""" 

159 

160 message = "Access denied: not the owner or admin" 

161 

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 

173 

174 # Then check admin 

175 admin_perm = IsAdmin() 

176 return await admin_perm.has_permission(source, info, **kwargs) 

177 

178 

179@singleton 

180class AllowAny(AbstractPermission): 

181 """Permission that allows any access (no restrictions).""" 

182 

183 message = "This should never be shown" 

184 

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 

193 

194 

195@singleton 

196class DenyAll(AbstractPermission): 

197 """Permission that denies all access.""" 

198 

199 message = "Access denied" 

200 

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 

209 

210 

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() 

220 

221 

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]