// Package main provides FX providers for Authz Service. // This file creates the service inline to avoid importing internal packages. package main import ( "context" "fmt" "net" authzv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/authz/v1" "git.dcentral.systems/toolz/goplt/internal/ent" "git.dcentral.systems/toolz/goplt/internal/ent/userrole" "git.dcentral.systems/toolz/goplt/pkg/config" "git.dcentral.systems/toolz/goplt/pkg/logger" "go.uber.org/fx" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/health" "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" ) // authzService provides authorization functionality. type authzService struct { client *ent.Client logger logger.Logger } // hasPermission checks if a user has a specific permission. func (s *authzService) hasPermission(ctx context.Context, userID, permCode string) (bool, error) { // Get user's roles userRoles, err := s.client.UserRole.Query(). Where(userrole.UserID(userID)). WithRole(func(rq *ent.RoleQuery) { rq.WithRolePermissions(func(rpq *ent.RolePermissionQuery) { rpq.WithPermission() }) }). All(ctx) if err != nil { return false, fmt.Errorf("failed to get user roles: %w", err) } // Check if any role has the permission for _, ur := range userRoles { role := ur.Edges.Role if role == nil { continue } rolePerms := role.Edges.RolePermissions for _, rp := range rolePerms { perm := rp.Edges.Permission if perm != nil && perm.Name == permCode { return true, nil } } } return false, nil } // getUserPermissions returns all permissions for a user. func (s *authzService) getUserPermissions(ctx context.Context, userID string) ([]*ent.Permission, error) { // Get user's roles userRoles, err := s.client.UserRole.Query(). Where(userrole.UserID(userID)). WithRole(func(rq *ent.RoleQuery) { rq.WithRolePermissions(func(rpq *ent.RolePermissionQuery) { rpq.WithPermission() }) }). All(ctx) if err != nil { return nil, fmt.Errorf("failed to get user roles: %w", err) } // Collect unique permissions permMap := make(map[string]*ent.Permission) for _, ur := range userRoles { role := ur.Edges.Role if role == nil { continue } rolePerms := role.Edges.RolePermissions for _, rp := range rolePerms { perm := rp.Edges.Permission if perm != nil { permMap[perm.ID] = perm } } } // Convert map to slice permissions := make([]*ent.Permission, 0, len(permMap)) for _, perm := range permMap { permissions = append(permissions, perm) } return permissions, nil } // getUserRoles returns all roles for a user. func (s *authzService) getUserRoles(ctx context.Context, userID string) ([]*ent.Role, error) { userRoles, err := s.client.UserRole.Query(). Where(userrole.UserID(userID)). WithRole(func(rq *ent.RoleQuery) { rq.WithRolePermissions(func(rpq *ent.RolePermissionQuery) { rpq.WithPermission() }) }). All(ctx) if err != nil { return nil, fmt.Errorf("failed to get user roles: %w", err) } roles := make([]*ent.Role, 0, len(userRoles)) for _, ur := range userRoles { if ur.Edges.Role != nil { roles = append(roles, ur.Edges.Role) } } return roles, nil } // authorize checks if a user has a specific permission. func (s *authzService) authorize(ctx context.Context, userID, permCode string) (bool, string, error) { hasPerm, err := s.hasPermission(ctx, userID, permCode) if err != nil { return false, "", err } if !hasPerm { return false, fmt.Sprintf("user %s does not have permission %s", userID, permCode), nil } return true, "authorized", nil } // authzServerImpl implements the AuthzService gRPC server. type authzServerImpl struct { authzv1.UnimplementedAuthzServiceServer service *authzService logger *zap.Logger } // Authorize checks if a user has a specific permission. func (s *authzServerImpl) Authorize(ctx context.Context, req *authzv1.AuthorizeRequest) (*authzv1.AuthorizeResponse, error) { authorized, message, err := s.service.authorize(ctx, req.UserId, req.Permission) if err != nil { return nil, status.Errorf(codes.Internal, "authorization check failed: %v", err) } return &authzv1.AuthorizeResponse{ Authorized: authorized, Message: message, }, nil } // HasPermission checks if a user has a specific permission. func (s *authzServerImpl) HasPermission(ctx context.Context, req *authzv1.HasPermissionRequest) (*authzv1.HasPermissionResponse, error) { hasPerm, err := s.service.hasPermission(ctx, req.UserId, req.Permission) if err != nil { return nil, status.Errorf(codes.Internal, "permission check failed: %v", err) } return &authzv1.HasPermissionResponse{ HasPermission: hasPerm, }, nil } // GetUserPermissions returns all permissions for a user. func (s *authzServerImpl) GetUserPermissions(ctx context.Context, req *authzv1.GetUserPermissionsRequest) (*authzv1.GetUserPermissionsResponse, error) { permissions, err := s.service.getUserPermissions(ctx, req.UserId) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user permissions: %v", err) } protoPerms := make([]*authzv1.Permission, 0, len(permissions)) for _, perm := range permissions { protoPerms = append(protoPerms, &authzv1.Permission{ Id: perm.ID, Code: perm.Name, // Permission.Name is the code (e.g., "blog.post.create") Name: perm.Name, Description: "", // Permission schema doesn't have description field }) } return &authzv1.GetUserPermissionsResponse{ Permissions: protoPerms, }, nil } // GetUserRoles returns all roles for a user. func (s *authzServerImpl) GetUserRoles(ctx context.Context, req *authzv1.GetUserRolesRequest) (*authzv1.GetUserRolesResponse, error) { roles, err := s.service.getUserRoles(ctx, req.UserId) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get user roles: %v", err) } protoRoles := make([]*authzv1.Role, 0, len(roles)) for _, role := range roles { // Get permission codes for this role permCodes := make([]string, 0) if role.Edges.RolePermissions != nil { for _, rp := range role.Edges.RolePermissions { if rp.Edges.Permission != nil { permCodes = append(permCodes, rp.Edges.Permission.Name) } } } protoRoles = append(protoRoles, &authzv1.Role{ Id: role.ID, Name: role.Name, Description: role.Description, Permissions: permCodes, }) } return &authzv1.GetUserRolesResponse{ Roles: protoRoles, }, nil } // provideAuthzService creates the authz service and gRPC server. func provideAuthzService() fx.Option { return fx.Options( // Authz service fx.Provide(func(client *ent.Client, log logger.Logger) (*authzService, error) { return &authzService{ client: client, logger: log, }, nil }), // gRPC server implementation fx.Provide(func(authzService *authzService, log logger.Logger) (*authzServerImpl, error) { zapLogger, _ := zap.NewProduction() return &authzServerImpl{ service: authzService, logger: zapLogger, }, nil }), // gRPC server wrapper fx.Provide(func( serverImpl *authzServerImpl, cfg config.ConfigProvider, log logger.Logger, ) (*grpcServerWrapper, error) { port := cfg.GetInt("services.authz.port") if port == 0 { port = 8083 } addr := fmt.Sprintf("0.0.0.0:%d", port) listener, err := net.Listen("tcp", addr) if err != nil { return nil, fmt.Errorf("failed to listen on %s: %w", addr, err) } grpcServer := grpc.NewServer() authzv1.RegisterAuthzServiceServer(grpcServer, serverImpl) // Register health service healthServer := health.NewServer() grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) healthServer.SetServingStatus("authz.v1.AuthzService", grpc_health_v1.HealthCheckResponse_SERVING) // Register reflection for grpcurl reflection.Register(grpcServer) return &grpcServerWrapper{ server: grpcServer, listener: listener, port: port, logger: log, }, nil }), ) }