package transport import ( "context" "crypto/sha256" "crypto/subtle" "errors" "strings" pb "git.sendico.io/tech/sxchnge/notifications/interface/notifications/v1" "git.sendico.io/tech/sxchnge/notifications/model" merrors "git.sendico.io/tech/sxchnge/shared/errors" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) type callerKey struct{} type API interface { Send(context.Context, string, model.Request) (*model.Notification, error) Get(context.Context, string, string) (*model.Notification, error) } type Server struct { pb.UnimplementedNotificationsServiceServer service API } func New(service API) *Server { return &Server{service: service} } func (s *Server) Send(ctx context.Context, r *pb.SendRequest) (*pb.SendResponse, error) { input, err := request(r) if err != nil { return nil, rpcError(err) } caller, _ := ctx.Value(callerKey{}).(string) n, err := s.service.Send(ctx, caller, input) if err != nil { return nil, rpcError(err) } return &pb.SendResponse{Notification: response(n)}, nil } func (s *Server) Get(ctx context.Context, r *pb.GetRequest) (*pb.GetResponse, error) { if r == nil { return nil, status.Error(codes.InvalidArgument, "missing request") } caller, _ := ctx.Value(callerKey{}).(string) n, err := s.service.Get(ctx, caller, r.Id) if err != nil { return nil, rpcError(err) } return &pb.GetResponse{Notification: response(n)}, nil } // Tokens are mapped to stable server-configured caller identities, never supplied IDs. func Authenticate(tokens map[string]string) grpc.UnaryServerInterceptor { hashes := map[string][32]byte{} for caller, token := range tokens { hashes[caller] = sha256.Sum256([]byte(token)) } return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { md, _ := metadata.FromIncomingContext(ctx) values := md.Get("authorization") if len(values) != 1 || !strings.HasPrefix(values[0], "Bearer ") { return nil, status.Error(codes.Unauthenticated, "invalid service credentials") } hash := sha256.Sum256([]byte(strings.TrimPrefix(values[0], "Bearer "))) identity := "" for caller, expected := range hashes { if subtle.ConstantTimeCompare(hash[:], expected[:]) == 1 { identity = caller } } if identity == "" { return nil, status.Error(codes.Unauthenticated, "invalid service credentials") } return handler(context.WithValue(ctx, callerKey{}, identity), req) } } func rpcError(err error) error { for _, pair := range []struct { err error code codes.Code message string }{ {merrors.ErrInvalidArg, codes.InvalidArgument, "invalid notification request"}, {merrors.ErrNoData, codes.NotFound, "notification or template not found"}, {merrors.ErrDataConflict, codes.AlreadyExists, "idempotency key already used"}, {merrors.ErrUnauthorized, codes.Unauthenticated, "missing service credentials"}, {merrors.ErrAccessDenied, codes.PermissionDenied, "access denied"}, {merrors.ErrNotImplemented, codes.Unimplemented, "channel is not implemented"}, {merrors.ErrServiceUnavailable, codes.Unavailable, "notification storage unavailable"}, } { if errors.Is(err, pair.err) { return status.Error(pair.code, pair.message) } } return status.Error(codes.Internal, "notification operation failed") }