Files
magistrala/fluxmq/api/grpc/server.go
T
Dušan Borovčanin 168e8b90cb
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
NOISSUE - Move rules engine, alarms, reports, journal and notifications to EE (#3552)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-07-30 17:55:57 +02:00

203 lines
6.2 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"strings"
"connectrpc.com/connect"
authv1 "github.com/absmach/fluxmq/pkg/proto/auth/v1"
"github.com/absmach/fluxmq/pkg/proto/auth/v1/authv1connect"
grpcChannelsV1 "github.com/absmach/magistrala/api/grpc/channels/v1"
grpcClientsV1 "github.com/absmach/magistrala/api/grpc/clients/v1"
apiutil "github.com/absmach/magistrala/api/http/util"
"github.com/absmach/magistrala/pkg/atom"
"github.com/absmach/magistrala/pkg/authn"
"github.com/absmach/magistrala/pkg/connections"
"github.com/absmach/magistrala/pkg/errors"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
"github.com/absmach/magistrala/pkg/messaging"
"github.com/absmach/magistrala/pkg/policies"
)
var _ authv1connect.AuthServiceHandler = (*connectServer)(nil)
type connectServer struct {
authv1connect.UnimplementedAuthServiceHandler
clients grpcClientsV1.ClientsServiceClient
channels grpcChannelsV1.ChannelsServiceClient
atomAuth atom.Authorizer
parser messaging.TopicParser
}
// NewServer creates a FluxMQ AuthService Connect handler that bridges to
// Magistrala's Clients (authn) and Channels (authz) services.
func NewServer(
clients grpcClientsV1.ClientsServiceClient,
channels grpcChannelsV1.ChannelsServiceClient,
parser messaging.TopicParser,
atomAuth ...atom.Authorizer,
) authv1connect.AuthServiceHandler {
var authz atom.Authorizer
if len(atomAuth) > 0 {
authz = atomAuth[0]
}
return &connectServer{
clients: clients,
channels: channels,
atomAuth: authz,
parser: parser,
}
}
func (s *connectServer) Authenticate(ctx context.Context, req *connect.Request[authv1.AuthnReq]) (*connect.Response[authv1.AuthnRes], error) {
username := req.Msg.GetUsername()
password := req.Msg.GetPassword()
// Reject empty-username clients early to avoid hitting Atom's rate limiter
// for an empty identifier, which would trigger the FluxMQ circuit breaker
// and block all subsequent MQTT connections.
if username == "" || password == "" {
return connect.NewResponse(&authv1.AuthnRes{Authenticated: false}), nil
}
token := authn.AuthPack(authn.BasicAuth, username, password)
res, err := s.clients.Authenticate(ctx, &grpcClientsV1.AuthnReq{Token: token})
if err != nil {
if !shouldTryDomainAuth(req.Msg, username, password) {
return nil, encodeError(err)
}
token = authn.AuthPack(authn.DomainAuth, username, password)
res, err = s.clients.Authenticate(ctx, &grpcClientsV1.AuthnReq{Token: token})
if err != nil {
return nil, encodeError(err)
}
}
return connect.NewResponse(&authv1.AuthnRes{
Authenticated: res.GetAuthenticated(),
Id: res.GetId(),
}), nil
}
func (s *connectServer) Authorize(ctx context.Context, req *connect.Request[authv1.AuthzReq]) (*connect.Response[authv1.AuthzRes], error) {
connType := connections.ConnType(req.Msg.GetAction())
if err := connections.CheckConnType(connType); err != nil {
return nil, encodeError(err)
}
var domainID, channelID string
var topicType messaging.TopicType
var err error
switch connType {
case connections.Publish:
domainID, channelID, _, topicType, err = s.parser.ParsePublishTopic(ctx, req.Msg.GetTopic(), true)
case connections.Subscribe:
domainID, channelID, _, topicType, err = s.parser.ParseSubscribeTopic(ctx, req.Msg.GetTopic(), true)
}
if err != nil {
if shouldDenyAuthorize(err) {
return connect.NewResponse(&authv1.AuthzRes{Authorized: false}), nil
}
return nil, encodeError(err)
}
if topicType == messaging.HealthType {
return connect.NewResponse(&authv1.AuthzRes{Authorized: true}), nil
}
if s.atomAuth != nil {
action := "subscribe"
if connType == connections.Publish {
action = "publish"
}
res, err := s.atomAuth.CheckAuthz(ctx, atom.AuthzRequest{
SubjectID: req.Msg.GetExternalId(),
Action: action,
ResourceID: channelID,
ObjectKind: "resource",
ObjectID: channelID,
Context: map[string]any{
"domain_id": domainID,
"client_type": policies.ClientType,
"connection": connType.String(),
"topic_type": uint32(topicType),
},
})
if err != nil {
if shouldDenyAuthorize(err) {
return connect.NewResponse(&authv1.AuthzRes{Authorized: false}), nil
}
return nil, encodeError(err)
}
return connect.NewResponse(&authv1.AuthzRes{Authorized: res.Allowed}), nil
}
ar := &grpcChannelsV1.AuthzReq{
Type: uint32(connType),
ClientId: req.Msg.GetExternalId(),
ClientType: policies.ClientType,
ChannelId: channelID,
DomainId: domainID,
}
res, err := s.channels.Authorize(ctx, ar)
if err != nil {
if shouldDenyAuthorize(err) {
return connect.NewResponse(&authv1.AuthzRes{Authorized: false}), nil
}
return nil, encodeError(err)
}
return connect.NewResponse(&authv1.AuthzRes{Authorized: res.GetAuthorized()}), nil
}
func shouldTryDomainAuth(msg *authv1.AuthnReq, username, password string) bool {
if username == "" || password == "" {
return false
}
return strings.HasPrefix(msg.GetClientId(), "http:")
}
func shouldDenyAuthorize(err error) bool {
if err == nil {
return false
}
switch {
case errors.Contains(err, svcerr.ErrAuthorization),
errors.Contains(err, svcerr.ErrNotFound),
errors.Contains(err, errors.ErrMalformedEntity),
errors.Contains(err, messaging.ErrMalformedTopic),
err == apiutil.ErrMissingID:
return true
}
// Backward compatibility for gRPC client layers that may return
// Internal with a payload containing "entity not found".
return strings.Contains(err.Error(), svcerr.ErrNotFound.Error())
}
func encodeError(err error) error {
switch {
case errors.Contains(err, nil):
return nil
case errors.Contains(err, errors.ErrMalformedEntity),
err == apiutil.ErrMissingID:
return connect.NewError(connect.CodeInvalidArgument, err)
case errors.Contains(err, svcerr.ErrAuthentication),
strings.Contains(err.Error(), "use of expired key"):
return connect.NewError(connect.CodeUnauthenticated, err)
case errors.Contains(err, svcerr.ErrAuthorization):
return connect.NewError(connect.CodePermissionDenied, err)
case errors.Contains(err, messaging.ErrMalformedTopic):
return connect.NewError(connect.CodeInvalidArgument, err)
default:
return connect.NewError(connect.CodeInternal, err)
}
}