- Implement Audit Service (2.5) - gRPC server with Record and Query operations - Database persistence with audit schema - Service registry integration - Entry point: cmd/audit-service - Implement Identity Service (2.2) - User CRUD operations - Password hashing with argon2id - Email verification and password reset flows - Entry point: cmd/identity-service - Fix package naming conflicts in user_service.go - Implement Auth Service (2.1) - JWT token generation and validation - Login, RefreshToken, ValidateToken, Logout RPCs - Integration with Identity Service - Entry point: cmd/auth-service - Note: RefreshToken entity needs Ent generation - Implement Authz Service (2.3, 2.4) - Permission checking and authorization - User roles and permissions retrieval - RBAC-based authorization - Entry point: cmd/authz-service - Implement gRPC clients for all services - Auth, Identity, Authz, and Audit clients - Service discovery integration - Full gRPC communication - Add service configurations to config/default.yaml - Create SUMMARY.md with implementation details and testing instructions - Fix compilation errors in Identity Service (password package conflicts) - All services build successfully and tests pass
287 lines
7.9 KiB
Go
287 lines
7.9 KiB
Go
// 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
|
|
}),
|
|
)
|
|
}
|