feat(grpc): add auth service transport

This commit is contained in:
2026-07-14 09:33:06 +03:30
parent 61f7b5c322
commit 0bf7268d9c
10 changed files with 686 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
syntax = "proto3";
package gapido.auth.v1;
service AuthService {
rpc RequestOtp (RequestOtpRequest) returns (RequestOtpResponse);
rpc VerifyOtp (VerifyOtpRequest) returns (TokenResponse);
rpc RefreshToken (RefreshTokenRequest) returns (TokenResponse);
rpc RevokeRefreshToken (RevokeRefreshTokenRequest) returns (RevokeRefreshTokenResponse);
rpc PublicPing (PingRequest) returns (PingResponse);
rpc UserOnly (ProtectedRequest) returns (ProtectedResponse);
rpc AdminOnly (ProtectedRequest) returns (ProtectedResponse);
}
message RequestOtpRequest {
string mobile = 1;
string purpose = 2;
}
message RequestOtpResponse {
bool accepted = 1;
}
message VerifyOtpRequest {
string mobile = 1;
string code = 2;
string purpose = 3;
}
message RefreshTokenRequest {
string refresh_token = 1;
}
message RevokeRefreshTokenRequest {
string refresh_token = 1;
}
message RevokeRefreshTokenResponse {
bool revoked = 1;
}
message TokenResponse {
string access_token = 1;
string refresh_token = 2;
string token_type = 3;
int32 expires_in = 4;
string role = 5;
}
message PingRequest {}
message PingResponse {
string message = 1;
}
message ProtectedRequest {}
message ProtectedResponse {
string user_id = 1;
string role = 2;
string message = 3;
}

View File

@@ -0,0 +1,2 @@
"""Generated protobuf modules are written here by gapido_auth.tools.generate_proto."""

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: gapido_auth/generated/auth.proto
# Protobuf Python Version: 5.28.1
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
28,
1,
'',
'gapido_auth/generated/auth.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n gapido_auth/generated/auth.proto\x12\x0egapido.auth.v1\"4\n\x11RequestOtpRequest\x12\x0e\n\x06mobile\x18\x01 \x01(\t\x12\x0f\n\x07purpose\x18\x02 \x01(\t\"&\n\x12RequestOtpResponse\x12\x10\n\x08\x61\x63\x63\x65pted\x18\x01 \x01(\x08\"A\n\x10VerifyOtpRequest\x12\x0e\n\x06mobile\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0f\n\x07purpose\x18\x03 \x01(\t\",\n\x13RefreshTokenRequest\x12\x15\n\rrefresh_token\x18\x01 \x01(\t\"2\n\x19RevokeRefreshTokenRequest\x12\x15\n\rrefresh_token\x18\x01 \x01(\t\"-\n\x1aRevokeRefreshTokenResponse\x12\x0f\n\x07revoked\x18\x01 \x01(\x08\"r\n\rTokenResponse\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\x12\x12\n\ntoken_type\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x05\x12\x0c\n\x04role\x18\x05 \x01(\t\"\r\n\x0bPingRequest\"\x1f\n\x0cPingResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\"\x12\n\x10ProtectedRequest\"C\n\x11ProtectedResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t2\xdd\x04\n\x0b\x41uthService\x12S\n\nRequestOtp\x12!.gapido.auth.v1.RequestOtpRequest\x1a\".gapido.auth.v1.RequestOtpResponse\x12L\n\tVerifyOtp\x12 .gapido.auth.v1.VerifyOtpRequest\x1a\x1d.gapido.auth.v1.TokenResponse\x12R\n\x0cRefreshToken\x12#.gapido.auth.v1.RefreshTokenRequest\x1a\x1d.gapido.auth.v1.TokenResponse\x12k\n\x12RevokeRefreshToken\x12).gapido.auth.v1.RevokeRefreshTokenRequest\x1a*.gapido.auth.v1.RevokeRefreshTokenResponse\x12G\n\nPublicPing\x12\x1b.gapido.auth.v1.PingRequest\x1a\x1c.gapido.auth.v1.PingResponse\x12O\n\x08UserOnly\x12 .gapido.auth.v1.ProtectedRequest\x1a!.gapido.auth.v1.ProtectedResponse\x12P\n\tAdminOnly\x12 .gapido.auth.v1.ProtectedRequest\x1a!.gapido.auth.v1.ProtectedResponseb\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gapido_auth.generated.auth_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_REQUESTOTPREQUEST']._serialized_start=52
_globals['_REQUESTOTPREQUEST']._serialized_end=104
_globals['_REQUESTOTPRESPONSE']._serialized_start=106
_globals['_REQUESTOTPRESPONSE']._serialized_end=144
_globals['_VERIFYOTPREQUEST']._serialized_start=146
_globals['_VERIFYOTPREQUEST']._serialized_end=211
_globals['_REFRESHTOKENREQUEST']._serialized_start=213
_globals['_REFRESHTOKENREQUEST']._serialized_end=257
_globals['_REVOKEREFRESHTOKENREQUEST']._serialized_start=259
_globals['_REVOKEREFRESHTOKENREQUEST']._serialized_end=309
_globals['_REVOKEREFRESHTOKENRESPONSE']._serialized_start=311
_globals['_REVOKEREFRESHTOKENRESPONSE']._serialized_end=356
_globals['_TOKENRESPONSE']._serialized_start=358
_globals['_TOKENRESPONSE']._serialized_end=472
_globals['_PINGREQUEST']._serialized_start=474
_globals['_PINGREQUEST']._serialized_end=487
_globals['_PINGRESPONSE']._serialized_start=489
_globals['_PINGRESPONSE']._serialized_end=520
_globals['_PROTECTEDREQUEST']._serialized_start=522
_globals['_PROTECTEDREQUEST']._serialized_end=540
_globals['_PROTECTEDRESPONSE']._serialized_start=542
_globals['_PROTECTEDRESPONSE']._serialized_end=609
_globals['_AUTHSERVICE']._serialized_start=612
_globals['_AUTHSERVICE']._serialized_end=1217
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,355 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
from gapido_auth.generated import auth_pb2 as gapido__auth_dot_generated_dot_auth__pb2
GRPC_GENERATED_VERSION = '1.68.1'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in gapido_auth/generated/auth_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
)
class AuthServiceStub(object):
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.RequestOtp = channel.unary_unary(
'/gapido.auth.v1.AuthService/RequestOtp',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.RequestOtpRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.RequestOtpResponse.FromString,
_registered_method=True)
self.VerifyOtp = channel.unary_unary(
'/gapido.auth.v1.AuthService/VerifyOtp',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.VerifyOtpRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.FromString,
_registered_method=True)
self.RefreshToken = channel.unary_unary(
'/gapido.auth.v1.AuthService/RefreshToken',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.RefreshTokenRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.FromString,
_registered_method=True)
self.RevokeRefreshToken = channel.unary_unary(
'/gapido.auth.v1.AuthService/RevokeRefreshToken',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenResponse.FromString,
_registered_method=True)
self.PublicPing = channel.unary_unary(
'/gapido.auth.v1.AuthService/PublicPing',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.PingRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.PingResponse.FromString,
_registered_method=True)
self.UserOnly = channel.unary_unary(
'/gapido.auth.v1.AuthService/UserOnly',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.FromString,
_registered_method=True)
self.AdminOnly = channel.unary_unary(
'/gapido.auth.v1.AuthService/AdminOnly',
request_serializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.SerializeToString,
response_deserializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.FromString,
_registered_method=True)
class AuthServiceServicer(object):
"""Missing associated documentation comment in .proto file."""
def RequestOtp(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def VerifyOtp(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def RefreshToken(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def RevokeRefreshToken(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def PublicPing(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def UserOnly(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def AdminOnly(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_AuthServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'RequestOtp': grpc.unary_unary_rpc_method_handler(
servicer.RequestOtp,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.RequestOtpRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.RequestOtpResponse.SerializeToString,
),
'VerifyOtp': grpc.unary_unary_rpc_method_handler(
servicer.VerifyOtp,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.VerifyOtpRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.SerializeToString,
),
'RefreshToken': grpc.unary_unary_rpc_method_handler(
servicer.RefreshToken,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.RefreshTokenRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.SerializeToString,
),
'RevokeRefreshToken': grpc.unary_unary_rpc_method_handler(
servicer.RevokeRefreshToken,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenResponse.SerializeToString,
),
'PublicPing': grpc.unary_unary_rpc_method_handler(
servicer.PublicPing,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.PingRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.PingResponse.SerializeToString,
),
'UserOnly': grpc.unary_unary_rpc_method_handler(
servicer.UserOnly,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.SerializeToString,
),
'AdminOnly': grpc.unary_unary_rpc_method_handler(
servicer.AdminOnly,
request_deserializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.FromString,
response_serializer=gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'gapido.auth.v1.AuthService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('gapido.auth.v1.AuthService', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class AuthService(object):
"""Missing associated documentation comment in .proto file."""
@staticmethod
def RequestOtp(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/RequestOtp',
gapido__auth_dot_generated_dot_auth__pb2.RequestOtpRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.RequestOtpResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def VerifyOtp(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/VerifyOtp',
gapido__auth_dot_generated_dot_auth__pb2.VerifyOtpRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def RefreshToken(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/RefreshToken',
gapido__auth_dot_generated_dot_auth__pb2.RefreshTokenRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.TokenResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def RevokeRefreshToken(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/RevokeRefreshToken',
gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.RevokeRefreshTokenResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def PublicPing(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/PublicPing',
gapido__auth_dot_generated_dot_auth__pb2.PingRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.PingResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def UserOnly(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/UserOnly',
gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def AdminOnly(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/gapido.auth.v1.AuthService/AdminOnly',
gapido__auth_dot_generated_dot_auth__pb2.ProtectedRequest.SerializeToString,
gapido__auth_dot_generated_dot_auth__pb2.ProtectedResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)

View File

@@ -0,0 +1,2 @@
"""Developer tooling."""

View File

@@ -0,0 +1,26 @@
from pathlib import Path
from grpc_tools import protoc
def main() -> None:
root = Path(__file__).resolve().parents[3]
proto_root = root / "proto"
src_root = root / "src"
proto_file = proto_root / "gapido_auth" / "generated" / "auth.proto"
result = protoc.main(
[
"grpc_tools.protoc",
f"-I{proto_root}",
f"--python_out={src_root}",
f"--grpc_python_out={src_root}",
str(proto_file),
]
)
if result != 0:
raise SystemExit(result)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,2 @@
"""Transport adapters."""

View File

@@ -0,0 +1,2 @@
"""gRPC transport."""

View File

@@ -0,0 +1,122 @@
# mypy: disable-error-code="attr-defined"
import logging
from collections.abc import Sequence
import grpc
from gapido_auth.application.auth_service import AuthService
from gapido_auth.domain.entities import Role, TokenPair, User
from gapido_auth.domain.errors import (
AppError,
AuthenticationError,
InactiveUser,
InvalidOtp,
OtpAttemptsExceeded,
OtpExpired,
PermissionDenied,
RateLimitExceeded,
ValidationError,
)
from gapido_auth.generated import auth_pb2, auth_pb2_grpc
logger = logging.getLogger(__name__)
class AuthGrpcServicer(auth_pb2_grpc.AuthServiceServicer):
def __init__(self, auth_service: AuthService) -> None:
self._auth_service = auth_service
async def RequestOtp(self, request, context): # type: ignore[no-untyped-def]
try:
await self._auth_service.request_otp(
mobile=request.mobile,
purpose=request.purpose or "login",
client_key=context.peer(),
)
return auth_pb2.RequestOtpResponse(accepted=True)
except AppError as exc:
await _abort_for_app_error(context, exc)
async def VerifyOtp(self, request, context): # type: ignore[no-untyped-def]
try:
token_pair = await self._auth_service.verify_otp(
mobile=request.mobile,
code=request.code,
purpose=request.purpose or "login",
)
return _token_response(token_pair)
except AppError as exc:
await _abort_for_app_error(context, exc)
async def RefreshToken(self, request, context): # type: ignore[no-untyped-def]
try:
token_pair = await self._auth_service.refresh_token(request.refresh_token)
return _token_response(token_pair)
except AppError as exc:
await _abort_for_app_error(context, exc)
async def RevokeRefreshToken(self, request, context): # type: ignore[no-untyped-def]
try:
token = _extract_bearer_token(context.invocation_metadata())
await self._auth_service.get_authenticated_user(token)
await self._auth_service.revoke_refresh_token(request.refresh_token)
return auth_pb2.RevokeRefreshTokenResponse(revoked=True)
except AppError as exc:
await _abort_for_app_error(context, exc)
async def PublicPing(self, request, context): # type: ignore[no-untyped-def]
return auth_pb2.PingResponse(message="public ok")
async def UserOnly(self, request, context): # type: ignore[no-untyped-def]
try:
user = await self._auth_service.get_authenticated_user(
_extract_bearer_token(context.invocation_metadata())
)
return _protected_response(user, "authenticated user ok")
except AppError as exc:
await _abort_for_app_error(context, exc)
async def AdminOnly(self, request, context): # type: ignore[no-untyped-def]
try:
user = await self._auth_service.require_role(
_extract_bearer_token(context.invocation_metadata()), Role.ADMIN
)
return _protected_response(user, "admin ok")
except AppError as exc:
await _abort_for_app_error(context, exc)
def _token_response(token_pair: TokenPair): # type: ignore[no-untyped-def]
return auth_pb2.TokenResponse(
access_token=token_pair.access_token,
refresh_token=token_pair.refresh_token,
token_type=token_pair.token_type,
expires_in=token_pair.expires_in,
role=token_pair.role.value,
)
def _protected_response(user: User, message: str): # type: ignore[no-untyped-def]
return auth_pb2.ProtectedResponse(user_id=user.id, role=user.role.value, message=message)
def _extract_bearer_token(metadata: Sequence[tuple[str, str]]) -> str:
for key, value in metadata:
if key.lower() == "authorization" and value.startswith("Bearer "):
return value.removeprefix("Bearer ").strip()
raise AuthenticationError("missing bearer token")
async def _abort_for_app_error(context: grpc.aio.ServicerContext, exc: AppError) -> None:
if isinstance(exc, AuthenticationError | InvalidOtp | OtpExpired | OtpAttemptsExceeded):
await context.abort(grpc.StatusCode.UNAUTHENTICATED, str(exc))
if isinstance(exc, InactiveUser):
await context.abort(grpc.StatusCode.PERMISSION_DENIED, str(exc))
if isinstance(exc, PermissionDenied):
await context.abort(grpc.StatusCode.PERMISSION_DENIED, str(exc))
if isinstance(exc, RateLimitExceeded):
await context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, str(exc))
if isinstance(exc, ValidationError):
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
logger.exception("Unhandled application error")
await context.abort(grpc.StatusCode.INTERNAL, "internal error")

View File

@@ -0,0 +1,54 @@
# mypy: disable-error-code="attr-defined,no-untyped-call"
import asyncio
import logging
import grpc
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
from grpc_reflection.v1alpha import reflection
from gapido_auth.config import get_settings
from gapido_auth.generated import auth_pb2, auth_pb2_grpc
from gapido_auth.infrastructure.container import build_auth_service
from gapido_auth.infrastructure.rabbitmq import connect_robust
from gapido_auth.transport.grpc.auth_servicer import AuthGrpcServicer
logger = logging.getLogger(__name__)
async def serve() -> None:
logging.basicConfig(level=logging.INFO)
settings = get_settings()
rabbitmq = await connect_robust(settings.rabbitmq_url)
channel = await rabbitmq.channel()
container = await build_auth_service(settings, channel)
server = grpc.aio.server()
auth_servicer = AuthGrpcServicer(container.auth_service)
auth_pb2_grpc.add_AuthServiceServicer_to_server(auth_servicer, server)
health_servicer = health.HealthServicer()
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
service_names = (
auth_pb2.DESCRIPTOR.services_by_name["AuthService"].full_name,
health.SERVICE_NAME,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(service_names, server)
health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
listen_addr = f"{settings.grpc_host}:{settings.grpc_port}"
server.add_insecure_port(listen_addr)
await server.start()
logger.info("gRPC auth service started on %s", listen_addr)
try:
await server.wait_for_termination()
finally:
await server.stop(grace=5)
await container.redis.aclose()
container.mongo_client.close()
await rabbitmq.close()
if __name__ == "__main__":
asyncio.run(serve())