Skip to content

Commit

Permalink
Add grpc authorizePAT
Browse files Browse the repository at this point in the history
Signed-off-by: nyagamunene <[email protected]>
  • Loading branch information
nyagamunene committed Oct 30, 2024
1 parent f11b434 commit 3008c8d
Show file tree
Hide file tree
Showing 34 changed files with 1,181 additions and 277 deletions.
43 changes: 43 additions & 0 deletions auth/api/grpc/auth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func NewAuthClient(conn *grpc.ClientConn, timeout time.Duration) grpcAuthV1.Auth
grpcAuthV1.AuthZRes{},
).Endpoint(),
timeout: timeout,
authorizePAT: kitgrpc.NewClient(

Check failure on line 47 in auth/api/grpc/auth/client.go

View workflow job for this annotation

GitHub Actions / Lint and Build

unknown field authorizePAT in struct literal of type authGrpcClient (typecheck)
conn,
authSvcName,
"AuthorizePAT",
encodeAuthorizePATRequest,
decodeAuthorizeResponse,
grpcAuthV1.AuthZRes{},
).Endpoint(),
timeout: timeout,

Check failure on line 55 in auth/api/grpc/auth/client.go

View workflow job for this annotation

GitHub Actions / Lint and Build

duplicate field name timeout in struct literal (typecheck)
}
}

Expand Down Expand Up @@ -109,3 +118,37 @@ func encodeAuthorizeRequest(_ context.Context, grpcReq interface{}) (interface{}
Object: req.Object,
}, nil
}

func (client authGrpcClient) AuthorizePAT(ctx context.Context, req *grpcAuthV1.AuthZReq, _ ...grpc.CallOption) (r *grpcAuthV1.AuthZRes, err error) {
ctx, cancel := context.WithTimeout(ctx, client.timeout)
defer cancel()

res, err := client.authorize(ctx, authReq{
Domain: req.GetDomain(),
SubjectType: req.GetSubjectType(),
Subject: req.GetSubject(),
SubjectKind: req.GetSubjectKind(),
Relation: req.GetRelation(),
Permission: req.GetPermission(),
ObjectType: req.GetObjectType(),
Object: req.GetObject(),
})
if err != nil {
return &grpcAuthV1.AuthZRes{}, grpcapi.DecodeError(err)
}

ar := res.(authorizeRes)
return &grpcAuthV1.AuthZRes{Authorized: ar.authorized, Id: ar.id}, nil
}

func encodeAuthorizePATRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(authPATReq)
return &grpcAuthV1.AuthZpatReq{
PaToken: req.paToken,
PlatformEntityType: req.platformEntityType,
OptionalDomainID: req.optionalDomainID,
OptionalDomainEntityType: req.optionalDomainEntityType,
Operation: req.operation,
EntityIDs: req.entityIDs,
}, nil
}
15 changes: 15 additions & 0 deletions auth/api/grpc/auth/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,18 @@ func authorizeEndpoint(svc auth.Service) endpoint.Endpoint {
return authorizeRes{authorized: true}, nil
}
}

func authorizePATEndpoint(svc auth.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(authPATReq)

if err := req.validate(); err != nil {
return authorizeRes{}, err
}
err := svc.AuthorizePAT(ctx, req.paToken, req.platformEntityType, req.optionalDomainID,req.optionalDomainEntityType, req.operation, req.entityIDs)
if err != nil {
return authorizeRes{authorized: false}, err
}
return authorizeRes{authorized: true}, nil
}
}
16 changes: 16 additions & 0 deletions auth/api/grpc/auth/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,19 @@ func (req authReq) validate() error {

return nil
}

type authPATReq struct {
paToken string
platformEntityType string
optionalDomainID string
optionalDomainEntityType string
operation string
entityIDs []string
}

func (req authPATReq) validate() error {
if req.paToken == "" {
return apiutil.ErrBearerToken
}
return nil
}
19 changes: 19 additions & 0 deletions auth/api/grpc/auth/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type authGrpcServer struct {
grpcAuthV1.UnimplementedAuthServiceServer
authorize kitgrpc.Handler
authenticate kitgrpc.Handler
authorizePAT kitgrpc.Handler
}

// NewAuthServer returns new AuthnServiceServer instance.
Expand All @@ -34,6 +35,12 @@ func NewAuthServer(svc auth.Service) grpcAuthV1.AuthServiceServer {
decodeAuthenticateRequest,
encodeAuthenticateResponse,
),

authorizePAT: kitgrpc.NewServer(
(authorizePATEndpoint(svc)),
decodeAuthorizePATRequest,
encodeAuthorizeResponse,
),
}
}

Expand Down Expand Up @@ -81,3 +88,15 @@ func encodeAuthorizeResponse(_ context.Context, grpcRes interface{}) (interface{
res := grpcRes.(authorizeRes)
return &grpcAuthV1.AuthZRes{Authorized: res.authorized, Id: res.id}, nil
}

func decodeAuthorizePATRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*grpcAuthV1.AuthZpatReq)
return authPATReq{
paToken: req.GetPaToken(),
platformEntityType: req.GetPlatformEntityType(),
optionalDomainID: req.GetOptionalDomainID(),
optionalDomainEntityType: req.GetOptionalDomainEntityType(),
operation: req.GetOperation(),
entityIDs: req.GetEntityIDs(),
}, nil
}
4 changes: 4 additions & 0 deletions auth/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (
RecoveryKey
// APIKey enables the one to act on behalf of the user.
APIKey
// PersonalAccessToken represents token generated by user for automation.
PersonalAccessToken
// InvitationKey is a key for inviting new users.
InvitationKey
)
Expand All @@ -44,6 +46,8 @@ func (kt KeyType) String() string {
return "recovery"
case APIKey:
return "API"
case PersonalAccessToken:
return "pat"
default:
return "unknown"
}
Expand Down
22 changes: 21 additions & 1 deletion auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/absmach/magistrala"
"github.com/absmach/magistrala/pat"
"github.com/absmach/magistrala/pkg/errors"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
"github.com/absmach/magistrala/pkg/policies"
Expand Down Expand Up @@ -87,6 +88,8 @@ var _ Service = (*service)(nil)

type service struct {
keys KeyRepository
pats pat.PATSRepository
hasher pat.Hasher
idProvider magistrala.IDProvider
evaluator policies.Evaluator
policysvc policies.Service
Expand All @@ -97,10 +100,12 @@ type service struct {
}

// New instantiates the auth service implementation.
func New(keys KeyRepository, idp magistrala.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
func New(keys KeyRepository,pats pat.PATSRepository, hasher pat.Hasher, idp magistrala.IDProvider, tokenizer Tokenizer, policyEvaluator policies.Evaluator, policyService policies.Service, loginDuration, refreshDuration, invitationDuration time.Duration) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
pats: pats,
hasher: hasher,
idProvider: idp,
evaluator: policyEvaluator,
policysvc: policyService,
Expand Down Expand Up @@ -151,6 +156,21 @@ func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, erro
}

func (svc service) Identify(ctx context.Context, token string) (Key, error) {
if strings.HasPrefix(token, "pat"+"_") {
pat, err := svc.IdentifyPAT(ctx, token)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / Lint and Build

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT) (typecheck)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / Lint and Build

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)) (typecheck)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / Lint and Build

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)) (typecheck)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / Lint and Build

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)) (typecheck)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / api-test

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / api-test

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / api-test

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)

Check failure on line 160 in auth/service.go

View workflow job for this annotation

GitHub Actions / api-test

svc.IdentifyPAT undefined (type service has no field or method IdentifyPAT)
if err != nil {
return Key{}, err
}
return Key{
ID: pat.ID,
Type: PersonalAccessToken,
Subject: pat.User,
User: pat.User,
IssuedAt: pat.IssuedAt,
ExpiresAt: pat.ExpiresAt,
}, nil
}

key, err := svc.tokenizer.Parse(token)
if errors.Contains(err, ErrExpiry) {
err = svc.keys.Remove(ctx, key.Issuer, key.ID)
Expand Down
29 changes: 26 additions & 3 deletions cmd/auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import (
"github.com/absmach/magistrala/auth/jwt"
apostgres "github.com/absmach/magistrala/auth/postgres"
"github.com/absmach/magistrala/auth/tracing"
boltclient "github.com/absmach/magistrala/internal/clients/bolt"
grpcAuthV1 "github.com/absmach/magistrala/internal/grpc/auth/v1"
grpcTokenV1 "github.com/absmach/magistrala/internal/grpc/token/v1"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pat/bolt"
"github.com/absmach/magistrala/pat/hasher"
"github.com/absmach/magistrala/pkg/jaeger"
"github.com/absmach/magistrala/pkg/policies/spicedb"
"github.com/absmach/magistrala/pkg/postgres"
Expand All @@ -39,6 +42,7 @@ import (
"github.com/authzed/grpcutil"
"github.com/caarlos0/env/v11"
"github.com/jmoiron/sqlx"
"go.etcd.io/bbolt"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
Expand All @@ -51,6 +55,7 @@ const (
envPrefixHTTP = "MG_AUTH_HTTP_"
envPrefixGrpc = "MG_AUTH_GRPC_"
envPrefixDB = "MG_AUTH_DB_"
envPrefixPATDB = "MG_AUTH_PAT_DB_"
defDB = "auth"
defSvcHTTPPort = "8189"
defSvcGRPCPort = "8181"
Expand Down Expand Up @@ -131,7 +136,23 @@ func main() {
exitCode = 1
return
}
svc := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient)

boltDBConfig := boltclient.Config{}
if err := env.ParseWithOptions(&boltDBConfig, env.Options{Prefix: envPrefixPATDB}); err != nil {
logger.Error(fmt.Sprintf("failed to parse bolt db config : %s\n", err.Error()))
exitCode = 1
return
}

bClient, err := boltclient.Connect(boltDBConfig, bolt.Init)
if err != nil {
logger.Error(fmt.Sprintf("failed to connect to bolt db : %s\n", err.Error()))
exitCode = 1
return
}
defer bClient.Close()

svc := newService(ctx, db, tracer, cfg, dbConfig, logger, spicedbclient, bClient, boltDBConfig)

grpcServerConfig := server.Config{Port: defSvcGRPCPort}
if err := env.ParseWithOptions(&grpcServerConfig, env.Options{Prefix: envPrefixGrpc}); err != nil {
Expand Down Expand Up @@ -211,17 +232,19 @@ func initSchema(ctx context.Context, client *authzed.ClientWithExperimental, sch
return nil
}

func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental) auth.Service {
func newService(_ context.Context, db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.Config, logger *slog.Logger, spicedbClient *authzed.ClientWithExperimental, bClient *bbolt.DB, bConfig boltclient.Config) auth.Service {
database := postgres.NewDatabase(db, dbConfig, tracer)
keysRepo := apostgres.New(database)
patsRepo := bolt.NewPATSRepository(bClient, bConfig.Bucket)
hasher := hasher.New()
idProvider := uuid.New()

pEvaluator := spicedb.NewPolicyEvaluator(spicedbClient, logger)
pService := spicedb.NewPolicyService(spicedbClient, logger)

t := jwt.New([]byte(cfg.SecretKey))

svc := auth.New(keysRepo, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration)
svc := auth.New(keysRepo,patsRepo, hasher, idProvider, t, pEvaluator, pService, cfg.AccessDuration, cfg.RefreshDuration, cfg.InvitationDuration)
svc = api.LoggingMiddleware(svc, logger)
counter, latency := prometheus.MakeMetrics("auth", "api")
svc = api.MetricsMiddleware(svc, counter, latency)
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ volumes:
magistrala-mqtt-broker-volume:
magistrala-spicedb-db-volume:
magistrala-auth-db-volume:
magistrala-pat-db-volume:
magistrala-domains-db-volume:
magistrala-invitations-db-volume:
magistrala-ui-db-volume:
Expand Down Expand Up @@ -135,6 +136,7 @@ services:
- magistrala-base-net
volumes:
- ./spicedb/schema.zed:${MG_SPICEDB_SCHEMA_FILE}
- magistrala-pat-db-volume:/magistrala-data
# Auth gRPC mTLS server certificates
- type: bind
source: ${MG_AUTH_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
Expand Down
7 changes: 7 additions & 0 deletions docker/nginx/nginx-key.conf
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ http {
proxy_pass http://domains:${MG_DOMAINS_HTTP_PORT};
}

# Proxy pass to auth service
location ~ ^/(domains|keys|pats) {
include snippets/proxy-headers.conf;
add_header Access-Control-Expose-Headers Location;
proxy_pass http://auth:${MG_AUTH_HTTP_PORT};
}

# Proxy pass to users service
location ~ ^/(users|password|authorize|oauth/callback/[^/]+) {
include snippets/proxy-headers.conf;
Expand Down
13 changes: 11 additions & 2 deletions internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package api
import (
"context"
"net/http"
"strings"

"github.com/absmach/magistrala/pkg/apiutil"
mgauthn "github.com/absmach/magistrala/pkg/authn"
Expand All @@ -14,7 +15,10 @@ import (

type sessionKeyType string

const SessionKey = sessionKeyType("session")
const (
SessionKey = sessionKeyType("session")
seperator = "_"
)

func AuthenticateMiddleware(authn mgauthn.Authentication, domainCheck bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
Expand All @@ -31,14 +35,19 @@ func AuthenticateMiddleware(authn mgauthn.Authentication, domainCheck bool) func
return
}

resp.Type = mgauthn.AccessToken
if strings.HasPrefix(token, "pat"+seperator) {
resp.Type = mgauthn.PersonalAccessToken
}

if domainCheck {
domain := chi.URLParam(r, "domainID")
if domain == "" {
EncodeError(r.Context(), apiutil.ErrMissingDomainID, w)
return
}
resp.DomainID = domain
resp.DomainUserID = domain + "_" + resp.UserID
resp.DomainUserID = domain + seperator + resp.UserID
}

ctx := context.WithValue(r.Context(), SessionKey, resp)
Expand Down
Loading

0 comments on commit 3008c8d

Please sign in to comment.