93 lines
3.3 KiB
Go
93 lines
3.3 KiB
Go
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")
|
|
}
|