mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
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
Signed-off-by: dusan <borovcanindusan1@gmail.com>
203 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|