Coverage for src/lexigram/auth/module.py: 86%

21 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 00:58 +0800

1"""Authentication and authorization module for dependency injection.""" 

2 

3from __future__ import annotations 

4 

5from typing import Any 

6 

7from lexigram.contracts.auth import ( 

8 AuthenticatorProtocol, 

9 AuthorizerProtocol, 

10 TokenManagerProtocol, 

11) 

12from lexigram.contracts.auth.protocols import PasswordHasherProtocol 

13from lexigram.di.module import DynamicModule, Module, module 

14 

15 

16@module(is_global=True) 

17class AuthModule(Module): 

18 """Full authentication and authorization stack (JWT, OAuth2/OIDC, RBAC, policies). 

19 

20 Call :meth:`configure` to configure the auth bundle with an 

21 :class:`~lexigram.auth.config.AuthConfig`. 

22 

23 Usage:: 

24 

25 from lexigram.auth.config import AuthConfig 

26 

27 @module( 

28 imports=[AuthModule.configure(AuthConfig(secret_key="..."))] 

29 ) 

30 class AppModule(Module): 

31 pass 

32 """ 

33 

34 @classmethod 

35 def configure( 

36 cls, 

37 config: Any | None = None, 

38 initial_roles: dict[str, Any] | None = None, 

39 is_global: bool = True, 

40 ) -> DynamicModule: 

41 """Create an AuthModule with explicit configuration. 

42 

43 Args: 

44 config: :class:`~lexigram.auth.config.AuthConfig` or ``None`` 

45 for framework defaults (development-only ephemeral secrets). 

46 initial_roles: Optional RBAC role seed forwarded to the 

47 authorization sub-provider. 

48 

49 Returns: 

50 A :class:`~lexigram.di.module.DynamicModule` descriptor. 

51 """ 

52 from lexigram.auth.admin.contributor import AuthAdminContributor 

53 from lexigram.auth.admin.handlers.active_sessions import ( 

54 ActiveSessionsWidgetHandler, 

55 ) 

56 from lexigram.auth.admin.handlers.failed_logins import FailedLoginsWidgetHandler 

57 from lexigram.auth.admin.handlers.token_refresh_rate import ( 

58 TokenRefreshRateWidgetHandler, 

59 ) 

60 from lexigram.auth.di.bundle_provider import AuthBundleProvider 

61 

62 return DynamicModule( 

63 module=cls, 

64 providers=[AuthBundleProvider(config=config, initial_roles=initial_roles)], 

65 exports=[ 

66 AuthenticatorProtocol, 

67 AuthorizerProtocol, 

68 TokenManagerProtocol, 

69 PasswordHasherProtocol, 

70 AuthAdminContributor, 

71 ActiveSessionsWidgetHandler, 

72 FailedLoginsWidgetHandler, 

73 TokenRefreshRateWidgetHandler, 

74 ], 

75 is_global=is_global, 

76 ) 

77 

78 @classmethod 

79 def stub(cls, config: Any = None) -> DynamicModule: 

80 """Return an in-memory AuthModule suitable for unit and integration testing. 

81 

82 Uses ephemeral in-memory storage with a fixed test secret key. 

83 No external token services, databases, or OAuth providers are 

84 configured. 

85 

86 Args: 

87 config: Optional test configuration override. 

88 

89 Returns: 

90 A DynamicModule backed by in-memory auth storage. 

91 """ 

92 from lexigram.auth.config import AuthConfig, JWTConfig 

93 from lexigram.auth.di.bundle_provider import AuthBundleProvider 

94 

95 return DynamicModule( 

96 module=cls, 

97 providers=[ 

98 AuthBundleProvider( 

99 config=AuthConfig( 

100 secret_key="test-secret-key-for-testing-only", # noqa: S106 # in-memory test bootstrap 

101 token=JWTConfig( 

102 secret_key="test-secret-key-for-testing-only", # noqa: S106 # in-memory test bootstrap 

103 ), 

104 ), 

105 initial_roles=None, 

106 ) 

107 ], 

108 exports=[ 

109 AuthenticatorProtocol, 

110 AuthorizerProtocol, 

111 TokenManagerProtocol, 

112 PasswordHasherProtocol, 

113 ], 

114 ) 

115 

116 

117__all__ = ["AuthModule"]