mirror of
https://github.com/hexolan/panels.git
synced 2026-03-26 12:40:21 +00:00
init auth-service
This commit is contained in:
0
services/auth-service/auth_service/__init__.py
Normal file
0
services/auth-service/auth_service/__init__.py
Normal file
80
services/auth-service/auth_service/events/base_consumer.py
Normal file
80
services/auth-service/auth_service/events/base_consumer.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from google.protobuf import message
|
||||
from aiokafka import AIOKafkaConsumer
|
||||
from aiokafka.structs import ConsumerRecord
|
||||
|
||||
from auth_service.models.config import Config
|
||||
from auth_service.models.service import AuthDBRepository
|
||||
|
||||
|
||||
class EventConsumer:
|
||||
"""An abstract consumer base class.
|
||||
|
||||
Attributes:
|
||||
CONSUMER_TOPIC: The topic to consume events from.
|
||||
CONSUMER_EVENT_TYPE (Type[message.Message]): The protobuf class type of the event msgs (used for deserialisation).
|
||||
_db_repo (Type[AuthDBRepository]): The repository interface for modifying data.
|
||||
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
|
||||
|
||||
"""
|
||||
CONSUMER_TOPIC: str
|
||||
CONSUMER_EVENT_TYPE: Type[message.Message]
|
||||
|
||||
def __init__(self, config: Config, db_repo: Type[AuthDBRepository]) -> None:
|
||||
"""Initialise the event consumer.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance (to access brokers list).
|
||||
db_repo (Type[AuthDBRepository]): The repository interface for updating data.
|
||||
|
||||
"""
|
||||
self._db_repo = db_repo
|
||||
self._consumer = AIOKafkaConsumer(
|
||||
self.CONSUMER_TOPIC,
|
||||
bootstrap_servers=config.kafka_brokers,
|
||||
group_id="auth-service"
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Begin consuming messages."""
|
||||
await self._consumer.start()
|
||||
try:
|
||||
async for msg in self._consumer:
|
||||
await self._process_msg(msg)
|
||||
except Exception as e:
|
||||
logging.error(f"error whilst consuming messages (on topic '{self.CONSUMER_TOPIC}'): {e}")
|
||||
finally:
|
||||
await self._consumer.stop()
|
||||
|
||||
async def _process_msg(self, msg: ConsumerRecord) -> None:
|
||||
"""Process a recieved message.
|
||||
|
||||
The messages are deserialise from bytes into their protobuf form,
|
||||
then passed to the `_process_event` method to handle the logic.
|
||||
|
||||
Args:
|
||||
msg (kafka.Message): The event to process.
|
||||
|
||||
"""
|
||||
try:
|
||||
event = self.CONSUMER_EVENT_TYPE()
|
||||
event.ParseFromString(msg.value)
|
||||
assert event.type != ""
|
||||
await self._process_event(event)
|
||||
except AssertionError:
|
||||
logging.error("invalid event recieved")
|
||||
return
|
||||
except Exception as e:
|
||||
logging.error("error whilst processing recieved event:", e)
|
||||
return
|
||||
|
||||
async def _process_event(self, event: Type[message.Message]) -> None:
|
||||
"""Process a recieved event.
|
||||
|
||||
Args:
|
||||
event (Type[message.Message]): The event serialised to protobuf form.
|
||||
|
||||
"""
|
||||
raise NotImplementedError("required consumer method (_process_event) not implemented")
|
||||
42
services/auth-service/auth_service/events/service.py
Normal file
42
services/auth-service/auth_service/events/service.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from typing import Type
|
||||
|
||||
from auth_service.models.config import Config
|
||||
from auth_service.models.service import AuthDBRepository
|
||||
from auth_service.events.user_consumer import UserEventConsumer
|
||||
|
||||
|
||||
class EventConsumersWrapper:
|
||||
"""A wrapper class for starting the event consumers.
|
||||
|
||||
Attributes:
|
||||
_user_consumer (UserEventConsumer): Wrapped consumer.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, user_consumer: UserEventConsumer) -> None:
|
||||
"""Add the consumers to the wrapper
|
||||
|
||||
Args:
|
||||
user_consumer (UserEventConsumer): Initialised user consumer.
|
||||
|
||||
"""
|
||||
self._user_consumer = user_consumer
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Begin consuming events on all the event consumers."""
|
||||
await self._user_consumer.start()
|
||||
|
||||
|
||||
def create_consumers(config: Config, db_repo: Type[AuthDBRepository]) -> EventConsumersWrapper:
|
||||
"""Initialse the event consumers and return them in a wrapper.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration instance.
|
||||
db_repo (Type[AuthDBRepository]): The database repo to pass to the consumers.
|
||||
|
||||
Returns:
|
||||
EventConsumerWrapper
|
||||
|
||||
"""
|
||||
user_consumer = UserEventConsumer(config, db_repo)
|
||||
return EventConsumersWrapper(user_consumer=user_consumer)
|
||||
32
services/auth-service/auth_service/events/user_consumer.py
Normal file
32
services/auth-service/auth_service/events/user_consumer.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
|
||||
from auth_service.models.proto import user_pb2
|
||||
from auth_service.events.base_consumer import EventConsumer
|
||||
|
||||
|
||||
class UserEventConsumer(EventConsumer):
|
||||
"""Consumer class responsible for 'user' events.
|
||||
|
||||
Attributes:
|
||||
CONSUMER_TOPIC: The topic to consume events from.
|
||||
CONSUMER_EVENT_TYPE (user_pb2.UserEvent): Kafka messages are serialised to this type.
|
||||
_db_repo (AuthDBRepository): The repository interface for modifying data.
|
||||
_consumer (aiokafka.AIOKafkaConsumer): The underlying Kafka instance.
|
||||
|
||||
"""
|
||||
CONSUMER_TOPIC = "user"
|
||||
CONSUMER_EVENT_TYPE = user_pb2.UserEvent
|
||||
|
||||
async def _process_event(self, event: user_pb2.UserEvent) -> None:
|
||||
"""Process a recieved event.
|
||||
|
||||
In response to a User deleted event, delete any auth methods
|
||||
this service has in relation to that user.
|
||||
|
||||
Args:
|
||||
event (user_pb2.UserEvent): The decoded protobuf message.
|
||||
|
||||
"""
|
||||
if event.type == "deleted":
|
||||
await self._db_repo.delete_password_auth_method(event.data.id)
|
||||
logging.info("succesfully processed UserEvent (type: 'deleted')")
|
||||
29
services/auth-service/auth_service/main.py
Normal file
29
services/auth-service/auth_service/main.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from sys import stdout
|
||||
|
||||
from auth_service.models.config import Config
|
||||
from auth_service.events.service import create_consumers
|
||||
from auth_service.postgres.service import create_db_repository
|
||||
from auth_service.service import ServiceRepository
|
||||
from auth_service.rpc.service import create_rpc_server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
config = Config()
|
||||
|
||||
db_repo = await create_db_repository(config)
|
||||
svc_repo = ServiceRepository(config, downstream_repo=db_repo)
|
||||
|
||||
rpc_server = create_rpc_server(svc_repo)
|
||||
event_consumers = create_consumers(config, db_repo=db_repo)
|
||||
|
||||
await asyncio.gather(
|
||||
rpc_server.start(),
|
||||
event_consumers.start()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(stream=stdout, level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
75
services/auth-service/auth_service/models/config.py
Normal file
75
services/auth-service/auth_service/models/config.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import base64
|
||||
from typing import Any, List
|
||||
|
||||
from pydantic import computed_field
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic_settings import BaseSettings, EnvSettingsSource
|
||||
from pydantic_settings.main import BaseSettings
|
||||
from pydantic_settings.sources import PydanticBaseSettingsSource
|
||||
|
||||
|
||||
class ConfigSource(EnvSettingsSource):
|
||||
"""Responsible for loading config options from environment variables."""
|
||||
def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any:
|
||||
if field_name == "kafka_brokers":
|
||||
# Comma delimit the kafka brokers.
|
||||
if value == None:
|
||||
return None
|
||||
return value.split(",")
|
||||
elif field_name == "jwt_public_key" or field_name == "jwt_private_key":
|
||||
# Decode the JWT public and private keys from base64.
|
||||
if value == None:
|
||||
return None
|
||||
return base64.standard_b64decode(value).decode(encoding="utf-8")
|
||||
|
||||
return super().prepare_field_value(field_name, field, value, value_is_complex)
|
||||
|
||||
|
||||
class Config(BaseSettings):
|
||||
"""The service configuration loaded from environment
|
||||
variables.
|
||||
|
||||
Attributes:
|
||||
postgres_user (str): Loaded from the 'POSTGRES_USER' envvar.
|
||||
postgres_pass (str): Loaded from the 'POSTGRES_PASS' envvar.
|
||||
postgres_host (str): Loaded from the 'POSTGRES_HOST' envvar.
|
||||
postgres_database (str): Loaded from the 'POSTGRES_DATABASE' envvar.
|
||||
kafka_brokers (list[str]): Loaded and comma delmited from the 'KAFKA_BROKERS' envvar.
|
||||
jwt_public_key (str): Loaded and decoded, from base64, from the 'JWT_PUBLIC_KEY' envvar.
|
||||
jwt_private_key (str): Loaded and decoded, from base64, from the 'JWT_PRIVATE_KEY' envvar.
|
||||
password_pepper (str): Loaded from the 'PASSWORD_PEPPER' envvar.
|
||||
postgres_dsn (str): Computed when accessed the first time. (@property)
|
||||
|
||||
"""
|
||||
postgres_user: str
|
||||
postgres_pass: str
|
||||
postgres_host: str
|
||||
postgres_database: str
|
||||
|
||||
kafka_brokers: List[str]
|
||||
|
||||
jwt_public_key: str
|
||||
jwt_private_key: str
|
||||
|
||||
password_pepper: str
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def postgres_dsn(self) -> str:
|
||||
"""Uses the postgres_user, postgres_pass, postgres_host,
|
||||
and postgres_database options to assemble a DSN.
|
||||
|
||||
Returns:
|
||||
str: DSN for connecting to the database.
|
||||
|
||||
"""
|
||||
return "postgresql+asyncpg://{user}:{password}@{host}/{db}".format(
|
||||
user=self.postgres_user,
|
||||
password=self.postgres_pass,
|
||||
host=self.postgres_host,
|
||||
db=self.postgres_database
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(cls, settings_cls: type[BaseSettings], *args, **kwargs) -> tuple[PydanticBaseSettingsSource, ...]:
|
||||
return (ConfigSource(settings_cls), )
|
||||
59
services/auth-service/auth_service/models/exceptions.py
Normal file
59
services/auth-service/auth_service/models/exceptions.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
from grpc import RpcContext, StatusCode
|
||||
|
||||
|
||||
class ServiceErrorCode(Enum):
|
||||
"""Error codes used for classifying ServiceExceptions."""
|
||||
INVALID_ARGUMENT = auto()
|
||||
CONFLICT = auto()
|
||||
NOT_FOUND = auto()
|
||||
INVALID_CREDENTIALS = auto()
|
||||
SERVICE_ERROR = auto()
|
||||
|
||||
__RPC_CODE_MAP__ = {
|
||||
INVALID_ARGUMENT: StatusCode.INVALID_ARGUMENT,
|
||||
CONFLICT: StatusCode.ALREADY_EXISTS,
|
||||
NOT_FOUND: StatusCode.NOT_FOUND,
|
||||
INVALID_CREDENTIALS: StatusCode.UNAUTHENTICATED,
|
||||
SERVICE_ERROR: StatusCode.INTERNAL
|
||||
}
|
||||
|
||||
def to_rpc_code(self) -> StatusCode:
|
||||
"""Convert a service error code to a gRPC status code.
|
||||
|
||||
Returns:
|
||||
The mapped RPC status code, if found, otherwise gRPC Unknown status code.
|
||||
|
||||
"""
|
||||
return self.__class__.__RPC_CODE_MAP__.get(self.value, StatusCode.UNKNOWN)
|
||||
|
||||
|
||||
class ServiceException(Exception):
|
||||
"""This exception provides an interface to convert service errors
|
||||
into gRPC errors, which can then be returned to the caller.
|
||||
|
||||
Args:
|
||||
msg (str): Error message.
|
||||
error_code (ServiceErrorCode): Categorisation code for the error.
|
||||
|
||||
Attributes:
|
||||
msg (str): The error message.
|
||||
error_code (ServiceErrorCode): Categorisation code for the error.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, msg: str, error_code: ServiceErrorCode) -> None:
|
||||
super().__init__(msg)
|
||||
self.msg = msg
|
||||
self.error_code = error_code
|
||||
|
||||
def apply_to_rpc(self, context: RpcContext) -> None:
|
||||
"""Apply the exception to an RPC context.
|
||||
|
||||
Args:
|
||||
context (grpc.RpcContext): The context to apply to.
|
||||
|
||||
"""
|
||||
context.set_code(self.error_code.to_rpc_code())
|
||||
context.set_details(self.msg)
|
||||
23
services/auth-service/auth_service/models/proto/auth_pb2.py
Normal file
23
services/auth-service/auth_service/models/proto/auth_pb2.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nauth.proto\x12\x0epanels.auth.v1\x1a\x1bgoogle/protobuf/empty.proto":\n\x15SetPasswordAuthMethod\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t"+\n\x18DeletePasswordAuthMethod\x12\x0f\n\x07user_id\x18\x01 \x01(\t"8\n\x13PasswordAuthRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t"`\n\tAuthToken\x12\x12\n\ntoken_type\x18\x01 \x01(\t\x12\x14\n\x0caccess_token\x18\x02 \x01(\t\x12\x15\n\rrefresh_token\x18\x03 \x01(\t\x12\x12\n\nexpires_in\x18\x04 \x01(\x032\x91\x02\n\x0bAuthService\x12T\n\x10AuthWithPassword\x12#.panels.auth.v1.PasswordAuthRequest\x1a\x19.panels.auth.v1.AuthToken"\x00\x12R\n\x0fSetPasswordAuth\x12%.panels.auth.v1.SetPasswordAuthMethod\x1a\x16.google.protobuf.Empty"\x00\x12X\n\x12DeletePasswordAuth\x12(.panels.auth.v1.DeletePasswordAuthMethod\x1a\x16.google.protobuf.Empty"\x00b\x06proto3')
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'auth_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_SETPASSWORDAUTHMETHOD']._serialized_start = 59
|
||||
_globals['_SETPASSWORDAUTHMETHOD']._serialized_end = 117
|
||||
_globals['_DELETEPASSWORDAUTHMETHOD']._serialized_start = 119
|
||||
_globals['_DELETEPASSWORDAUTHMETHOD']._serialized_end = 162
|
||||
_globals['_PASSWORDAUTHREQUEST']._serialized_start = 164
|
||||
_globals['_PASSWORDAUTHREQUEST']._serialized_end = 220
|
||||
_globals['_AUTHTOKEN']._serialized_start = 222
|
||||
_globals['_AUTHTOKEN']._serialized_end = 318
|
||||
_globals['_AUTHSERVICE']._serialized_start = 321
|
||||
_globals['_AUTHSERVICE']._serialized_end = 594
|
||||
47
services/auth-service/auth_service/models/proto/auth_pb2.pyi
Normal file
47
services/auth-service/auth_service/models/proto/auth_pb2.pyi
Normal file
@@ -0,0 +1,47 @@
|
||||
from google.protobuf import empty_pb2 as _empty_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Optional as _Optional
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class SetPasswordAuthMethod(_message.Message):
|
||||
__slots__ = ['user_id', 'password']
|
||||
USER_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PASSWORD_FIELD_NUMBER: _ClassVar[int]
|
||||
user_id: str
|
||||
password: str
|
||||
|
||||
def __init__(self, user_id: _Optional[str]=..., password: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class DeletePasswordAuthMethod(_message.Message):
|
||||
__slots__ = ['user_id']
|
||||
USER_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
user_id: str
|
||||
|
||||
def __init__(self, user_id: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class PasswordAuthRequest(_message.Message):
|
||||
__slots__ = ['user_id', 'password']
|
||||
USER_ID_FIELD_NUMBER: _ClassVar[int]
|
||||
PASSWORD_FIELD_NUMBER: _ClassVar[int]
|
||||
user_id: str
|
||||
password: str
|
||||
|
||||
def __init__(self, user_id: _Optional[str]=..., password: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class AuthToken(_message.Message):
|
||||
__slots__ = ['token_type', 'access_token', 'refresh_token', 'expires_in']
|
||||
TOKEN_TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
ACCESS_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
||||
REFRESH_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
||||
EXPIRES_IN_FIELD_NUMBER: _ClassVar[int]
|
||||
token_type: str
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
expires_in: int
|
||||
|
||||
def __init__(self, token_type: _Optional[str]=..., access_token: _Optional[str]=..., refresh_token: _Optional[str]=..., expires_in: _Optional[int]=...) -> None:
|
||||
...
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
from . import auth_pb2 as auth__pb2
|
||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||
|
||||
class AuthServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.AuthWithPassword = channel.unary_unary('/panels.auth.v1.AuthService/AuthWithPassword', request_serializer=auth__pb2.PasswordAuthRequest.SerializeToString, response_deserializer=auth__pb2.AuthToken.FromString)
|
||||
self.SetPasswordAuth = channel.unary_unary('/panels.auth.v1.AuthService/SetPasswordAuth', request_serializer=auth__pb2.SetPasswordAuthMethod.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
|
||||
self.DeletePasswordAuth = channel.unary_unary('/panels.auth.v1.AuthService/DeletePasswordAuth', request_serializer=auth__pb2.DeletePasswordAuthMethod.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
|
||||
|
||||
class AuthServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def AuthWithPassword(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 SetPasswordAuth(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 DeletePasswordAuth(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 = {'AuthWithPassword': grpc.unary_unary_rpc_method_handler(servicer.AuthWithPassword, request_deserializer=auth__pb2.PasswordAuthRequest.FromString, response_serializer=auth__pb2.AuthToken.SerializeToString), 'SetPasswordAuth': grpc.unary_unary_rpc_method_handler(servicer.SetPasswordAuth, request_deserializer=auth__pb2.SetPasswordAuthMethod.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString), 'DeletePasswordAuth': grpc.unary_unary_rpc_method_handler(servicer.DeletePasswordAuth, request_deserializer=auth__pb2.DeletePasswordAuthMethod.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString)}
|
||||
generic_handler = grpc.method_handlers_generic_handler('panels.auth.v1.AuthService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
class AuthService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def AuthWithPassword(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, '/panels.auth.v1.AuthService/AuthWithPassword', auth__pb2.PasswordAuthRequest.SerializeToString, auth__pb2.AuthToken.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def SetPasswordAuth(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, '/panels.auth.v1.AuthService/SetPasswordAuth', auth__pb2.SetPasswordAuthMethod.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def DeletePasswordAuth(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, '/panels.auth.v1.AuthService/DeletePasswordAuth', auth__pb2.DeletePasswordAuthMethod.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
36
services/auth-service/auth_service/models/proto/user_pb2.py
Normal file
36
services/auth-service/auth_service/models/proto/user_pb2.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_sym_db = _symbol_database.Default()
|
||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuser.proto\x12\x0epanels.user.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto"\x96\x01\n\x04User\x12\n\n\x02id\x18\x01 \x01(\t\x12\x10\n\x08username\x18\x02 \x01(\t\x12\x10\n\x08is_admin\x18\x03 \x01(\x08\x12.\n\ncreated_at\x18\x04 \x01(\x0b2\x1a.google.protobuf.Timestamp\x12.\n\nupdated_at\x18\x05 \x01(\x0b2\x1a.google.protobuf.Timestamp"1\n\x0bUserMutable\x12\x15\n\x08username\x18\x01 \x01(\tH\x00\x88\x01\x01B\x0b\n\t_username">\n\x11CreateUserRequest\x12)\n\x04data\x18\x01 \x01(\x0b2\x1b.panels.user.v1.UserMutable" \n\x12GetUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t"(\n\x14GetUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t"N\n\x15UpdateUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12)\n\x04data\x18\x02 \x01(\x0b2\x1b.panels.user.v1.UserMutable"V\n\x17UpdateUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12)\n\x04data\x18\x02 \x01(\x0b2\x1b.panels.user.v1.UserMutable"#\n\x15DeleteUserByIdRequest\x12\n\n\x02id\x18\x01 \x01(\t"+\n\x17DeleteUserByNameRequest\x12\x10\n\x08username\x18\x01 \x01(\t"=\n\tUserEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12"\n\x04data\x18\x02 \x01(\x0b2\x14.panels.user.v1.User2\xb4\x04\n\x0bUserService\x12G\n\nCreateUser\x12!.panels.user.v1.CreateUserRequest\x1a\x14.panels.user.v1.User"\x00\x12E\n\x07GetUser\x12".panels.user.v1.GetUserByIdRequest\x1a\x14.panels.user.v1.User"\x00\x12M\n\rGetUserByName\x12$.panels.user.v1.GetUserByNameRequest\x1a\x14.panels.user.v1.User"\x00\x12K\n\nUpdateUser\x12%.panels.user.v1.UpdateUserByIdRequest\x1a\x14.panels.user.v1.User"\x00\x12S\n\x10UpdateUserByName\x12\'.panels.user.v1.UpdateUserByNameRequest\x1a\x14.panels.user.v1.User"\x00\x12M\n\nDeleteUser\x12%.panels.user.v1.DeleteUserByIdRequest\x1a\x16.google.protobuf.Empty"\x00\x12U\n\x10DeleteUserByName\x12\'.panels.user.v1.DeleteUserByNameRequest\x1a\x16.google.protobuf.Empty"\x00b\x06proto3')
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'user_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
DESCRIPTOR._options = None
|
||||
_globals['_USER']._serialized_start = 93
|
||||
_globals['_USER']._serialized_end = 243
|
||||
_globals['_USERMUTABLE']._serialized_start = 245
|
||||
_globals['_USERMUTABLE']._serialized_end = 294
|
||||
_globals['_CREATEUSERREQUEST']._serialized_start = 296
|
||||
_globals['_CREATEUSERREQUEST']._serialized_end = 358
|
||||
_globals['_GETUSERBYIDREQUEST']._serialized_start = 360
|
||||
_globals['_GETUSERBYIDREQUEST']._serialized_end = 392
|
||||
_globals['_GETUSERBYNAMEREQUEST']._serialized_start = 394
|
||||
_globals['_GETUSERBYNAMEREQUEST']._serialized_end = 434
|
||||
_globals['_UPDATEUSERBYIDREQUEST']._serialized_start = 436
|
||||
_globals['_UPDATEUSERBYIDREQUEST']._serialized_end = 514
|
||||
_globals['_UPDATEUSERBYNAMEREQUEST']._serialized_start = 516
|
||||
_globals['_UPDATEUSERBYNAMEREQUEST']._serialized_end = 602
|
||||
_globals['_DELETEUSERBYIDREQUEST']._serialized_start = 604
|
||||
_globals['_DELETEUSERBYIDREQUEST']._serialized_end = 639
|
||||
_globals['_DELETEUSERBYNAMEREQUEST']._serialized_start = 641
|
||||
_globals['_DELETEUSERBYNAMEREQUEST']._serialized_end = 684
|
||||
_globals['_USEREVENT']._serialized_start = 686
|
||||
_globals['_USEREVENT']._serialized_end = 747
|
||||
_globals['_USERSERVICE']._serialized_start = 750
|
||||
_globals['_USERSERVICE']._serialized_end = 1314
|
||||
100
services/auth-service/auth_service/models/proto/user_pb2.pyi
Normal file
100
services/auth-service/auth_service/models/proto/user_pb2.pyi
Normal file
@@ -0,0 +1,100 @@
|
||||
from google.protobuf import empty_pb2 as _empty_pb2
|
||||
from google.protobuf import timestamp_pb2 as _timestamp_pb2
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
class User(_message.Message):
|
||||
__slots__ = ['id', 'username', 'is_admin', 'created_at', 'updated_at']
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
USERNAME_FIELD_NUMBER: _ClassVar[int]
|
||||
IS_ADMIN_FIELD_NUMBER: _ClassVar[int]
|
||||
CREATED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
|
||||
id: str
|
||||
username: str
|
||||
is_admin: bool
|
||||
created_at: _timestamp_pb2.Timestamp
|
||||
updated_at: _timestamp_pb2.Timestamp
|
||||
|
||||
def __init__(self, id: _Optional[str]=..., username: _Optional[str]=..., is_admin: bool=..., created_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=..., updated_at: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]]=...) -> None:
|
||||
...
|
||||
|
||||
class UserMutable(_message.Message):
|
||||
__slots__ = ['username']
|
||||
USERNAME_FIELD_NUMBER: _ClassVar[int]
|
||||
username: str
|
||||
|
||||
def __init__(self, username: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class CreateUserRequest(_message.Message):
|
||||
__slots__ = ['data']
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
data: UserMutable
|
||||
|
||||
def __init__(self, data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
|
||||
...
|
||||
|
||||
class GetUserByIdRequest(_message.Message):
|
||||
__slots__ = ['id']
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
id: str
|
||||
|
||||
def __init__(self, id: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class GetUserByNameRequest(_message.Message):
|
||||
__slots__ = ['username']
|
||||
USERNAME_FIELD_NUMBER: _ClassVar[int]
|
||||
username: str
|
||||
|
||||
def __init__(self, username: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class UpdateUserByIdRequest(_message.Message):
|
||||
__slots__ = ['id', 'data']
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
id: str
|
||||
data: UserMutable
|
||||
|
||||
def __init__(self, id: _Optional[str]=..., data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
|
||||
...
|
||||
|
||||
class UpdateUserByNameRequest(_message.Message):
|
||||
__slots__ = ['username', 'data']
|
||||
USERNAME_FIELD_NUMBER: _ClassVar[int]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
username: str
|
||||
data: UserMutable
|
||||
|
||||
def __init__(self, username: _Optional[str]=..., data: _Optional[_Union[UserMutable, _Mapping]]=...) -> None:
|
||||
...
|
||||
|
||||
class DeleteUserByIdRequest(_message.Message):
|
||||
__slots__ = ['id']
|
||||
ID_FIELD_NUMBER: _ClassVar[int]
|
||||
id: str
|
||||
|
||||
def __init__(self, id: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class DeleteUserByNameRequest(_message.Message):
|
||||
__slots__ = ['username']
|
||||
USERNAME_FIELD_NUMBER: _ClassVar[int]
|
||||
username: str
|
||||
|
||||
def __init__(self, username: _Optional[str]=...) -> None:
|
||||
...
|
||||
|
||||
class UserEvent(_message.Message):
|
||||
__slots__ = ['type', 'data']
|
||||
TYPE_FIELD_NUMBER: _ClassVar[int]
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
type: str
|
||||
data: User
|
||||
|
||||
def __init__(self, type: _Optional[str]=..., data: _Optional[_Union[User, _Mapping]]=...) -> None:
|
||||
...
|
||||
102
services/auth-service/auth_service/models/proto/user_pb2_grpc.py
Normal file
102
services/auth-service/auth_service/models/proto/user_pb2_grpc.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
import grpc
|
||||
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
|
||||
from . import user_pb2 as user__pb2
|
||||
|
||||
class UserServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def __init__(self, channel):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.CreateUser = channel.unary_unary('/panels.user.v1.UserService/CreateUser', request_serializer=user__pb2.CreateUserRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
|
||||
self.GetUser = channel.unary_unary('/panels.user.v1.UserService/GetUser', request_serializer=user__pb2.GetUserByIdRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
|
||||
self.GetUserByName = channel.unary_unary('/panels.user.v1.UserService/GetUserByName', request_serializer=user__pb2.GetUserByNameRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
|
||||
self.UpdateUser = channel.unary_unary('/panels.user.v1.UserService/UpdateUser', request_serializer=user__pb2.UpdateUserByIdRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
|
||||
self.UpdateUserByName = channel.unary_unary('/panels.user.v1.UserService/UpdateUserByName', request_serializer=user__pb2.UpdateUserByNameRequest.SerializeToString, response_deserializer=user__pb2.User.FromString)
|
||||
self.DeleteUser = channel.unary_unary('/panels.user.v1.UserService/DeleteUser', request_serializer=user__pb2.DeleteUserByIdRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
|
||||
self.DeleteUserByName = channel.unary_unary('/panels.user.v1.UserService/DeleteUserByName', request_serializer=user__pb2.DeleteUserByNameRequest.SerializeToString, response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString)
|
||||
|
||||
class UserServiceServicer(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
def CreateUser(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 GetUser(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 GetUserByName(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 UpdateUser(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 UpdateUserByName(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 DeleteUser(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 DeleteUserByName(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_UserServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {'CreateUser': grpc.unary_unary_rpc_method_handler(servicer.CreateUser, request_deserializer=user__pb2.CreateUserRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'GetUser': grpc.unary_unary_rpc_method_handler(servicer.GetUser, request_deserializer=user__pb2.GetUserByIdRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'GetUserByName': grpc.unary_unary_rpc_method_handler(servicer.GetUserByName, request_deserializer=user__pb2.GetUserByNameRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'UpdateUser': grpc.unary_unary_rpc_method_handler(servicer.UpdateUser, request_deserializer=user__pb2.UpdateUserByIdRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'UpdateUserByName': grpc.unary_unary_rpc_method_handler(servicer.UpdateUserByName, request_deserializer=user__pb2.UpdateUserByNameRequest.FromString, response_serializer=user__pb2.User.SerializeToString), 'DeleteUser': grpc.unary_unary_rpc_method_handler(servicer.DeleteUser, request_deserializer=user__pb2.DeleteUserByIdRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString), 'DeleteUserByName': grpc.unary_unary_rpc_method_handler(servicer.DeleteUserByName, request_deserializer=user__pb2.DeleteUserByNameRequest.FromString, response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString)}
|
||||
generic_handler = grpc.method_handlers_generic_handler('panels.user.v1.UserService', rpc_method_handlers)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
|
||||
class UserService(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def CreateUser(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, '/panels.user.v1.UserService/CreateUser', user__pb2.CreateUserRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def GetUser(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, '/panels.user.v1.UserService/GetUser', user__pb2.GetUserByIdRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def GetUserByName(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, '/panels.user.v1.UserService/GetUserByName', user__pb2.GetUserByNameRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def UpdateUser(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, '/panels.user.v1.UserService/UpdateUser', user__pb2.UpdateUserByIdRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def UpdateUserByName(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, '/panels.user.v1.UserService/UpdateUserByName', user__pb2.UpdateUserByNameRequest.SerializeToString, user__pb2.User.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def DeleteUser(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, '/panels.user.v1.UserService/DeleteUser', user__pb2.DeleteUserByIdRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
|
||||
@staticmethod
|
||||
def DeleteUserByName(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, '/panels.user.v1.UserService/DeleteUserByName', user__pb2.DeleteUserByNameRequest.SerializeToString, google_dot_protobuf_dot_empty__pb2.Empty.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
|
||||
55
services/auth-service/auth_service/models/service.py
Normal file
55
services/auth-service/auth_service/models/service.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from time import time
|
||||
from typing import Optional
|
||||
from datetime import timedelta
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
|
||||
from auth_service.models.proto import auth_pb2
|
||||
from auth_service.models.exceptions import ServiceException, ServiceErrorCode
|
||||
|
||||
|
||||
class AuthRecord(BaseModel):
|
||||
user_id: str
|
||||
password: SecretStr # Hashed Password
|
||||
|
||||
|
||||
class AuthToken(BaseModel):
|
||||
token_type: str = "Bearer"
|
||||
access_token: str
|
||||
expires_in: int = Field(default=int(timedelta(minutes=30).total_seconds()))
|
||||
# refresh_token: str # todo: implement functionality in the future
|
||||
|
||||
@classmethod
|
||||
def to_protobuf(cls, auth_token: "AuthToken") -> auth_pb2.AuthToken:
|
||||
return auth_pb2.AuthToken(**auth_token.model_dump())
|
||||
|
||||
|
||||
class AccessTokenClaims(BaseModel):
|
||||
sub: str
|
||||
iat: int = Field(default_factory=lambda: int(time()))
|
||||
exp: int = Field(default_factory=lambda: int(time() + timedelta(minutes=30).total_seconds()))
|
||||
|
||||
|
||||
class AuthRepository:
|
||||
"""Abstract repository interface"""
|
||||
async def auth_with_password(self, user_id: str, password: str) -> AuthToken:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
async def set_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
async def delete_password_auth_method(self, user_id: str) -> None:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
|
||||
class AuthDBRepository(AuthRepository):
|
||||
"""Abstract database repository interface"""
|
||||
async def get_auth_record(self, user_id: str) -> Optional[AuthRecord]:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
async def create_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
async def update_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
raise ServiceException("unimplemented internal repository method", ServiceErrorCode.SERVICE_ERROR)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS auth_methods CASCADE;
|
||||
@@ -0,0 +1,4 @@
|
||||
CREATE TABLE auth_methods (
|
||||
"user_id" varchar(64) PRIMARY KEY,
|
||||
"password" varchar(128) NOT NULL
|
||||
);
|
||||
39
services/auth-service/auth_service/postgres/repository.py
Normal file
39
services/auth-service/auth_service/postgres/repository.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Optional
|
||||
|
||||
from databases import Database
|
||||
|
||||
from auth_service.models.service import AuthDBRepository, AuthRecord
|
||||
|
||||
|
||||
class ServiceDBRepository(AuthDBRepository):
|
||||
"""Database repository responsible for CRUD actions
|
||||
on the database.
|
||||
|
||||
This repository will be utilised by other upstream repositories
|
||||
or the Kafka event consumers.
|
||||
|
||||
Attributes:
|
||||
_db (Database): The postgres database connection handler.
|
||||
|
||||
"""
|
||||
def __init__(self, db: Database) -> None:
|
||||
self._db = db
|
||||
|
||||
async def get_auth_record(self, user_id: str) -> Optional[AuthRecord]:
|
||||
query = "SELECT user_id, password FROM auth_methods WHERE user_id = :user_id"
|
||||
result = await self._db.fetch_one(query=query, values={"user_id": user_id})
|
||||
if result is None:
|
||||
return None
|
||||
return AuthRecord(user_id=result["user_id"], password=result["password"])
|
||||
|
||||
async def create_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
query = "INSERT INTO auth_methods (user_id, password) VALUES (:user_id, :password)"
|
||||
await self._db.execute(query=query, values={"user_id": user_id, "password": password})
|
||||
|
||||
async def update_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
query = "UPDATE auth_methods SET password = :password WHERE user_id = :user_id"
|
||||
await self._db.execute(query=query, values={"user_id": user_id, "password": password})
|
||||
|
||||
async def delete_password_auth_method(self, user_id: str) -> None:
|
||||
query = "DELETE FROM auth_methods WHERE user_id = :user_id"
|
||||
await self._db.execute(query=query, values={"user_id": user_id})
|
||||
40
services/auth-service/auth_service/postgres/service.py
Normal file
40
services/auth-service/auth_service/postgres/service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
|
||||
from databases import Database
|
||||
|
||||
from auth_service.models.config import Config
|
||||
from auth_service.postgres.repository import ServiceDBRepository
|
||||
|
||||
|
||||
async def connect_database(config: Config) -> Database:
|
||||
"""Opens a connection to the database.
|
||||
|
||||
Args:
|
||||
config (Config): The app configuration.
|
||||
|
||||
Returns:
|
||||
A connected databases.Database instance
|
||||
|
||||
"""
|
||||
db = Database(config.postgres_dsn)
|
||||
try:
|
||||
await db.connect()
|
||||
except Exception:
|
||||
logging.error("failed to connect to postgresql database")
|
||||
raise
|
||||
|
||||
return db
|
||||
|
||||
|
||||
async def create_db_repository(config: Config) -> ServiceDBRepository:
|
||||
"""Create the database repository.
|
||||
|
||||
Open a database connection and instantialise the
|
||||
database repository.
|
||||
|
||||
Returns:
|
||||
ServiceDBRepository
|
||||
|
||||
"""
|
||||
db = await connect_database(config)
|
||||
return ServiceDBRepository(db)
|
||||
0
services/auth-service/auth_service/rpc/__init__.py
Normal file
0
services/auth-service/auth_service/rpc/__init__.py
Normal file
185
services/auth-service/auth_service/rpc/auth.py
Normal file
185
services/auth-service/auth_service/rpc/auth.py
Normal file
@@ -0,0 +1,185 @@
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Type
|
||||
|
||||
from google.protobuf import empty_pb2
|
||||
from grpc import RpcContext, StatusCode
|
||||
|
||||
from auth_service.models.exceptions import ServiceException
|
||||
from auth_service.models.service import AuthRepository, AuthToken
|
||||
from auth_service.models.proto import auth_pb2, auth_pb2_grpc
|
||||
|
||||
|
||||
class AuthServicer(auth_pb2_grpc.AuthServiceServicer):
|
||||
"""Contains definitions for the service's RPC methods.
|
||||
|
||||
The request attributes are validated and translated
|
||||
from protobufs into business model form, then passed
|
||||
along to the service repository to handling the
|
||||
business logic.
|
||||
|
||||
Responses from calls to methods in the service repository
|
||||
are then translated back to protobuf form to return
|
||||
to the user.
|
||||
|
||||
Attributes:
|
||||
_svc_repo (Type[AuthRepository]): The highest level service repository.
|
||||
|
||||
"""
|
||||
def __init__(self, svc_repo: Type[AuthRepository]) -> None:
|
||||
self._svc_repo = svc_repo
|
||||
|
||||
def _apply_error(self, context: RpcContext, code: StatusCode, msg: str) -> None:
|
||||
"""Set an error on a given RPC request.
|
||||
|
||||
Args:
|
||||
context (grpc.RpcContext): The context to apply the error to.
|
||||
code (grpc.StatusCode): The gRPC status code.
|
||||
msg (str): The error details.
|
||||
|
||||
"""
|
||||
context.set_code(code)
|
||||
context.set_details(msg)
|
||||
|
||||
def _apply_unknown_error(self, context: RpcContext) -> None:
|
||||
"""Apply a de facto error fallback message.
|
||||
|
||||
Args:
|
||||
context (grpc.RpcContext): The context to apply the error to.
|
||||
|
||||
"""
|
||||
self._apply_error(context, StatusCode.UNKNOWN, "unknown error occured")
|
||||
|
||||
async def AuthWithPassword(self, request: auth_pb2.PasswordAuthRequest, context: RpcContext) -> auth_pb2.AuthToken:
|
||||
"""AuthWithPassword RPC Call
|
||||
|
||||
Args:
|
||||
request (auth_pb2.PasswordAuthRequest): The request parameters.
|
||||
context (grpc.RpcContext): The context of the RPC call.
|
||||
|
||||
Returns:
|
||||
auth_pb2.AuthToken: With a succesfully authentication.
|
||||
|
||||
"""
|
||||
# validate the request inputs
|
||||
if request.user_id == "":
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="user not provided"
|
||||
)
|
||||
return
|
||||
|
||||
if request.password == "":
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="password not provided"
|
||||
)
|
||||
return
|
||||
|
||||
if len(request.password) < 8:
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="invalid password"
|
||||
)
|
||||
return
|
||||
|
||||
# Attempt to authenticate the user
|
||||
try:
|
||||
token = await self._svc_repo.auth_with_password(request.user_id, request.password)
|
||||
except ServiceException as err:
|
||||
err.apply_to_rpc(context)
|
||||
return
|
||||
except Exception:
|
||||
logging.error(traceback.format_exc())
|
||||
self._apply_unknown_error(context)
|
||||
return
|
||||
|
||||
# Convert token to protobuf
|
||||
return AuthToken.to_protobuf(token)
|
||||
|
||||
async def SetPasswordAuth(self, request: auth_pb2.SetPasswordAuthMethod, context: RpcContext) -> empty_pb2.Empty:
|
||||
"""SetPasswordAuth RPC Call
|
||||
|
||||
Args:
|
||||
request (auth_pb2.SetPasswordAuthMethod): The request parameters.
|
||||
context (grpc.RpcContext): The context of the RPC call.
|
||||
|
||||
Returns:
|
||||
empty_pb2.Empty: Empty protobuf response (in effect returns None).
|
||||
|
||||
"""
|
||||
# validate the request inputs
|
||||
if request.user_id == "":
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="user id not provided"
|
||||
)
|
||||
return
|
||||
|
||||
if request.password == "":
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="password not provided"
|
||||
)
|
||||
return
|
||||
|
||||
if len(request.password) < 8:
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="password must be at least 8 characters"
|
||||
)
|
||||
return
|
||||
|
||||
# Attempt to create the auth method
|
||||
try:
|
||||
await self._svc_repo.set_password_auth_method(request.user_id, request.password)
|
||||
except ServiceException as err:
|
||||
err.apply_to_rpc(context)
|
||||
return
|
||||
except Exception:
|
||||
logging.error(traceback.format_exc())
|
||||
self._apply_unknown_error(context)
|
||||
return
|
||||
|
||||
# Success
|
||||
return empty_pb2.Empty()
|
||||
|
||||
async def DeletePasswordAuth(self, request: auth_pb2.DeletePasswordAuthMethod, context: RpcContext) -> empty_pb2.Empty:
|
||||
"""DeletePasswordAuth RPC Call
|
||||
|
||||
Args:
|
||||
request (auth_pb2.DeletePasswordAuthMethod): The request parameters.
|
||||
context (grpc.RpcContext): The context of the RPC call.
|
||||
|
||||
Returns:
|
||||
empty_pb2.Empty: Empty protobuf response (in effect returns None).
|
||||
|
||||
"""
|
||||
# Ensure a user id is provided
|
||||
if request.user_id == "":
|
||||
self._apply_error(
|
||||
context,
|
||||
code=StatusCode.INVALID_ARGUMENT,
|
||||
msg="user id not provided"
|
||||
)
|
||||
return
|
||||
|
||||
# Attempt to delete the auth method
|
||||
try:
|
||||
await self._svc_repo.delete_password_auth_method(request.user_id, request.password)
|
||||
except ServiceException as err:
|
||||
err.apply_to_rpc(context)
|
||||
return
|
||||
except Exception:
|
||||
logging.error(traceback.format_exc())
|
||||
self._apply_unknown_error(context)
|
||||
return
|
||||
|
||||
# Success
|
||||
return empty_pb2.Empty()
|
||||
52
services/auth-service/auth_service/rpc/service.py
Normal file
52
services/auth-service/auth_service/rpc/service.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
import grpc
|
||||
from grpc_health.v1 import health, health_pb2_grpc
|
||||
|
||||
from auth_service.models.proto import auth_pb2_grpc
|
||||
from auth_service.models.service import AuthRepository
|
||||
from auth_service.rpc.auth import AuthServicer
|
||||
|
||||
|
||||
class RPCServerWrapper:
|
||||
"""A wrapper class for the RPC server.
|
||||
|
||||
Attributes:
|
||||
_grpc_server (grpc.aio.Server): The gRPC server instance.
|
||||
|
||||
"""
|
||||
def __init__(self, svc_repo: Type[AuthRepository]) -> None:
|
||||
"""Creates the gRPC server and adds the servicers.
|
||||
|
||||
Args:
|
||||
svc_repo (Type[AuthRepository]): The service repository to pass to the servicers.
|
||||
|
||||
"""
|
||||
self._grpc_server = grpc.aio.server()
|
||||
self._grpc_server.add_insecure_port("[::]:9090")
|
||||
|
||||
auth_servicer = AuthServicer(svc_repo)
|
||||
auth_pb2_grpc.add_AuthServiceServicer_to_server(auth_servicer, self._grpc_server)
|
||||
|
||||
health_servicer = health.aio.HealthServicer()
|
||||
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self._grpc_server)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Begin serving RPC asynchronously."""
|
||||
logging.info("attempting to serve RPC...")
|
||||
await self._grpc_server.start()
|
||||
await self._grpc_server.wait_for_termination()
|
||||
|
||||
|
||||
def create_rpc_server(svc_repo: Type[AuthRepository]) -> RPCServerWrapper:
|
||||
"""Instantialise the RPC server wrapper.
|
||||
|
||||
Args:
|
||||
svc_repo (Type[AuthRepository]): The service repository for the RPC servicers to interface with.
|
||||
|
||||
Returns:
|
||||
RPCServerWrapper
|
||||
|
||||
"""
|
||||
return RPCServerWrapper(svc_repo)
|
||||
161
services/auth-service/auth_service/service.py
Normal file
161
services/auth-service/auth_service/service.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import hmac
|
||||
import hashlib
|
||||
from typing import Type
|
||||
|
||||
import jwt
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import VerificationError
|
||||
|
||||
from auth_service.models.config import Config
|
||||
from auth_service.models.exceptions import ServiceException, ServiceErrorCode
|
||||
from auth_service.models.service import AuthRepository, AuthDBRepository, AuthToken, AccessTokenClaims
|
||||
|
||||
|
||||
class ServiceRepository(AuthRepository):
|
||||
"""The service repository responsible for handling
|
||||
the business logic.
|
||||
|
||||
As I've developed most of the services in this project
|
||||
using a repository pattern, this provides a level
|
||||
of abstraction, allowing me to swap out the downstream
|
||||
repositories to add additional layers.
|
||||
|
||||
For example, instead of have the next downstream
|
||||
repository (`_repo` attribute) point directly
|
||||
to the DB repo instance, I could point it to a
|
||||
Redis repository to use as a caching layer, as I
|
||||
have done so in some of the other services in this
|
||||
project.
|
||||
|
||||
Attributes:
|
||||
_repo (AuthDBRepository): The downstream database repository.
|
||||
_jwt_private (str): RSA private key for producing JWT tokens.
|
||||
_hasher (argon2.PasswordHasher): Library class utilised for hashing passwords.
|
||||
_pepper (bytes): An additional 'secret salt' applied to all passwords when hashed.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config: Config, downstream_repo: Type[AuthDBRepository]) -> None:
|
||||
self._repo = downstream_repo
|
||||
|
||||
self._jwt_private = config.jwt_private_key
|
||||
|
||||
self._hasher = PasswordHasher() # Use default RFC_9106_LOW_MEMORY profile
|
||||
self._pepper = bytes(config.password_pepper, encoding="utf-8")
|
||||
|
||||
def _apply_pepper(self, password: str) -> str:
|
||||
"""Apply the pepper ('secret salt') to
|
||||
a given password using HMAC.
|
||||
|
||||
Args:
|
||||
password (str): The password to apply to pepper to.
|
||||
|
||||
Returns:
|
||||
str: The password with applied pepper (in hexadecimal form).
|
||||
|
||||
"""
|
||||
return hmac.new(key=self._pepper, msg=bytes(password, encoding="utf-8"), digestmod=hashlib.sha256).hexdigest()
|
||||
|
||||
def _hash_password(self, password: str) -> str:
|
||||
"""Hash a given password.
|
||||
|
||||
The pepper is applied to the password, and the result
|
||||
is then hashed using Argon2id.
|
||||
|
||||
Args:
|
||||
password (str): The password to hash.
|
||||
|
||||
Returns:
|
||||
str: The hashed password.
|
||||
|
||||
"""
|
||||
return self._hasher.hash(self._apply_pepper(password))
|
||||
|
||||
def _verify_password(self, hash: str, password: str) -> None:
|
||||
"""Verify that an input password matches
|
||||
a hash.
|
||||
|
||||
The pepper is applied to the input password
|
||||
and compared to the stored hash.
|
||||
|
||||
Args:
|
||||
hash (str): The existing hashed password.
|
||||
password (str): The password to verify against the hash.
|
||||
|
||||
Raises:
|
||||
argon2.Exceptions.VerificationError: if the password does not match
|
||||
|
||||
"""
|
||||
self._hasher.verify(hash, self._apply_pepper(password))
|
||||
|
||||
def _issue_auth_token(self, user_id: str) -> AuthToken:
|
||||
"""Issue an auth token.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to issue the tokens to.
|
||||
|
||||
Returns:
|
||||
AuthToken: A response containing the generated access token,
|
||||
token type and expiry time.
|
||||
"""
|
||||
claims = AccessTokenClaims(sub=user_id)
|
||||
access_token = jwt.encode(claims.model_dump(), self._jwt_private, algorithm="RS256")
|
||||
return AuthToken(access_token=access_token)
|
||||
|
||||
async def auth_with_password(self, user_id: str, password: str) -> AuthToken:
|
||||
"""Attempt to authenticate a user with a
|
||||
provided password.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to attempt to authenticate.
|
||||
password (str): The password to verify against.
|
||||
|
||||
"""
|
||||
# Get the auth record for that user
|
||||
auth_record = await self._repo.get_auth_record(user_id)
|
||||
if not auth_record:
|
||||
raise ServiceException("invalid user id or password", ServiceErrorCode.INVALID_CREDENTIALS)
|
||||
|
||||
# Verify the password hashes
|
||||
try:
|
||||
self._verify_password(auth_record.password.get_secret_value(), password)
|
||||
except VerificationError:
|
||||
raise ServiceException("invalid user id or password", ServiceErrorCode.INVALID_CREDENTIALS)
|
||||
|
||||
# Update the auth record if the password needs rehash
|
||||
if self._hasher.check_needs_rehash(auth_record.password.get_secret_value()):
|
||||
await self._repo.update_password_auth_method(auth_record.user_id, self._hash_password(password))
|
||||
|
||||
# Issue a token for the user
|
||||
return self._issue_auth_token(auth_record.user_id)
|
||||
|
||||
async def set_password_auth_method(self, user_id: str, password: str) -> None:
|
||||
"""Set a user's password for use when authenticating.
|
||||
|
||||
If the specified user does not already have an existing
|
||||
authentication method, then insert a record, otherwise
|
||||
update their existing record.
|
||||
|
||||
Args:
|
||||
user_id (str): The referenced user.
|
||||
password (str): The new password for that user.
|
||||
|
||||
"""
|
||||
# Hash the password
|
||||
password = self._hash_password(password)
|
||||
|
||||
# Update the auth method or create one if it doesn't exist
|
||||
auth_record = await self._repo.get_auth_record(user_id)
|
||||
if auth_record is not None:
|
||||
await self._repo.update_password_auth_method(user_id, password)
|
||||
else:
|
||||
await self._repo.create_password_auth_method(user_id, password)
|
||||
|
||||
async def delete_password_auth_method(self, user_id: str) -> None:
|
||||
"""Delete a user's authentication method.
|
||||
|
||||
Args:
|
||||
user_id (str): The referenced user.
|
||||
|
||||
"""
|
||||
await self._repo.delete_password_auth_method(user_id)
|
||||
Reference in New Issue
Block a user