Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions apiserver/internal/apis/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package apis

import (
"net/http"
"strconv"

authMW "dkhalife.com/tasks/core/internal/middleware/auth"
"dkhalife.com/tasks/core/internal/models"
kubeconfigService "dkhalife.com/tasks/core/internal/services/kubeconfig"
"dkhalife.com/tasks/core/internal/utils/auth"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-gonic/gin"
)

type KubeconfigAPIHandler struct {
service *kubeconfigService.KubeconfigService
}

func KubeconfigAPI(service *kubeconfigService.KubeconfigService) *KubeconfigAPIHandler {
return &KubeconfigAPIHandler{
service: service,
}
}

// importKubeconfig handles importing a kubeconfig YAML
func (h *KubeconfigAPIHandler) importKubeconfig(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)

var req models.ImportKubeconfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request",
"message": err.Error(),
})
return
}

status, response := h.service.ImportKubeconfig(c, currentIdentity.UserID, req.KubeconfigYAML)
c.JSON(status, response)
}

// listContexts handles listing all contexts for the current user
func (h *KubeconfigAPIHandler) listContexts(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)
status, response := h.service.ListContexts(c, currentIdentity.UserID)
c.JSON(status, response)
}

// getContext handles retrieving a specific context
func (h *KubeconfigAPIHandler) getContext(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)

contextID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid context ID",
})
return
}

status, response := h.service.GetContext(c, currentIdentity.UserID, contextID)
c.JSON(status, response)
}

// setActiveContext handles setting a context as active
func (h *KubeconfigAPIHandler) setActiveContext(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)

var req models.SetActiveContextRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request",
"message": err.Error(),
})
return
}

status, response := h.service.SetActiveContext(c, currentIdentity.UserID, req.ContextID)
c.JSON(status, response)
}

// deleteContext handles deleting a context
func (h *KubeconfigAPIHandler) deleteContext(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)

contextID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid context ID",
})
return
}

status, response := h.service.DeleteContext(c, currentIdentity.UserID, contextID)
c.JSON(status, response)
}

// getActiveContext handles retrieving the active context
func (h *KubeconfigAPIHandler) getActiveContext(c *gin.Context) {
currentIdentity := auth.CurrentIdentity(c)
status, response := h.service.GetActiveContext(c, currentIdentity.UserID)
c.JSON(status, response)
}

// KubeconfigRoutes registers the kubeconfig routes
func KubeconfigRoutes(r *gin.Engine, h *KubeconfigAPIHandler, authGate *jwt.GinJWTMiddleware) {
kubeconfigRoutes := r.Group("api/v1/kubeconfig")
kubeconfigRoutes.Use(authGate.MiddlewareFunc())
{
kubeconfigRoutes.POST("/import", authMW.ScopeMiddleware(models.ApiTokenScopeUserWrite), h.importKubeconfig)
kubeconfigRoutes.GET("", authMW.ScopeMiddleware(models.ApiTokenScopeUserRead), h.listContexts)
kubeconfigRoutes.GET("/active", authMW.ScopeMiddleware(models.ApiTokenScopeUserRead), h.getActiveContext)
kubeconfigRoutes.GET("/:id", authMW.ScopeMiddleware(models.ApiTokenScopeUserRead), h.getContext)
kubeconfigRoutes.PUT("/active", authMW.ScopeMiddleware(models.ApiTokenScopeUserWrite), h.setActiveContext)
kubeconfigRoutes.DELETE("/:id", authMW.ScopeMiddleware(models.ApiTokenScopeUserWrite), h.deleteContext)
}
}
71 changes: 71 additions & 0 deletions apiserver/internal/models/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package models

import "time"

// KubeContext represents a Kubernetes context stored in the database
type KubeContext struct {
ID int `json:"id" gorm:"primary_key"`
UserID int `json:"user_id" gorm:"column:user_id;not null;index"`
Name string `json:"name" gorm:"column:name;not null"`
IsActive bool `json:"is_active" gorm:"column:is_active;default:false"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;default:CURRENT_TIMESTAMP"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;default:NULL;autoUpdateTime"`

// Cluster information
ClusterName string `json:"cluster_name" gorm:"column:cluster_name;not null"`
Server string `json:"server" gorm:"column:server;not null"`
CertificateAuthorityData string `json:"-" gorm:"column:certificate_authority_data;type:text"` // Base64 encoded, encrypted

// User information
ContextUser string `json:"context_user" gorm:"column:context_user;not null"`
Token string `json:"-" gorm:"column:token;type:text"` // Encrypted
ClientCertificate string `json:"-" gorm:"column:client_certificate;type:text"` // Base64 encoded, encrypted
ClientKey string `json:"-" gorm:"column:client_key;type:text"` // Base64 encoded, encrypted

// Foreign key
User User `json:"-" gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;"`
}

// KubeContextResponse represents the response returned to clients (with masked sensitive data)
type KubeContextResponse struct {
ID int `json:"id"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
ClusterName string `json:"cluster_name"`
Server string `json:"server"`
ContextUser string `json:"context_user"`
HasToken bool `json:"has_token"`
HasCert bool `json:"has_cert"`
HasKey bool `json:"has_key"`
HasCA bool `json:"has_ca"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// ToResponse converts a KubeContext to KubeContextResponse with masked sensitive data
func (k *KubeContext) ToResponse() KubeContextResponse {
return KubeContextResponse{
ID: k.ID,
Name: k.Name,
IsActive: k.IsActive,
ClusterName: k.ClusterName,
Server: k.Server,
ContextUser: k.ContextUser,
HasToken: k.Token != "",
HasCert: k.ClientCertificate != "",
HasKey: k.ClientKey != "",
HasCA: k.CertificateAuthorityData != "",
CreatedAt: k.CreatedAt,
UpdatedAt: k.UpdatedAt,
}
}

// ImportKubeconfigRequest represents the request to import a kubeconfig
type ImportKubeconfigRequest struct {
KubeconfigYAML string `json:"kubeconfig_yaml" binding:"required"`
}

// SetActiveContextRequest represents the request to set an active context
type SetActiveContextRequest struct {
ContextID int `json:"context_id" binding:"required"`
}
201 changes: 201 additions & 0 deletions apiserver/internal/repos/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package repos

import (
"context"
"fmt"

"dkhalife.com/tasks/core/config"
"dkhalife.com/tasks/core/internal/models"
"dkhalife.com/tasks/core/internal/utils/encryption"
"gorm.io/gorm"
)

type IKubeconfigRepo interface {
CreateContext(ctx context.Context, kubeContext *models.KubeContext) error
GetContextByID(ctx context.Context, userID, contextID int) (*models.KubeContext, error)
GetAllContexts(ctx context.Context, userID int) ([]models.KubeContext, error)
SetActiveContext(ctx context.Context, userID, contextID int) error
DeleteContext(ctx context.Context, userID, contextID int) error
GetActiveContext(ctx context.Context, userID int) (*models.KubeContext, error)
}

type KubeconfigRepo struct {
db *gorm.DB
encryptor *encryption.Encryptor
}

var _ IKubeconfigRepo = (*KubeconfigRepo)(nil)

func NewKubeconfigRepository(db *gorm.DB, cfg *config.Config) IKubeconfigRepo {
return &KubeconfigRepo{
db: db,
encryptor: encryption.NewEncryptor(cfg.Jwt.Secret),
}
}

// CreateContext creates a new kubeconfig context with encrypted sensitive data
func (r *KubeconfigRepo) CreateContext(ctx context.Context, kubeContext *models.KubeContext) error {
// Encrypt sensitive fields
var err error
if kubeContext.Token != "" {
kubeContext.Token, err = r.encryptor.Encrypt(kubeContext.Token)
if err != nil {
return fmt.Errorf("failed to encrypt token: %w", err)
}
}

if kubeContext.ClientCertificate != "" {
kubeContext.ClientCertificate, err = r.encryptor.Encrypt(kubeContext.ClientCertificate)
if err != nil {
return fmt.Errorf("failed to encrypt client certificate: %w", err)
}
}

if kubeContext.ClientKey != "" {
kubeContext.ClientKey, err = r.encryptor.Encrypt(kubeContext.ClientKey)
if err != nil {
return fmt.Errorf("failed to encrypt client key: %w", err)
}
}

if kubeContext.CertificateAuthorityData != "" {
kubeContext.CertificateAuthorityData, err = r.encryptor.Encrypt(kubeContext.CertificateAuthorityData)
if err != nil {
return fmt.Errorf("failed to encrypt CA data: %w", err)
}
}

return r.db.WithContext(ctx).Create(kubeContext).Error
}

// GetContextByID retrieves a kubeconfig context by ID with decrypted sensitive data
func (r *KubeconfigRepo) GetContextByID(ctx context.Context, userID, contextID int) (*models.KubeContext, error) {
var kubeContext models.KubeContext
err := r.db.WithContext(ctx).
Where("id = ? AND user_id = ?", contextID, userID).
First(&kubeContext).Error
if err != nil {
return nil, err
}

// Decrypt sensitive fields
if err := r.decryptContext(&kubeContext); err != nil {
return nil, err
}

return &kubeContext, nil
}

// GetAllContexts retrieves all kubeconfig contexts for a user
func (r *KubeconfigRepo) GetAllContexts(ctx context.Context, userID int) ([]models.KubeContext, error) {
var contexts []models.KubeContext
err := r.db.WithContext(ctx).
Where("user_id = ?", userID).
Order("created_at DESC").
Find(&contexts).Error
if err != nil {
return nil, err
}

// Decrypt sensitive fields for all contexts
for i := range contexts {
if err := r.decryptContext(&contexts[i]); err != nil {
return nil, err
}
}

return contexts, nil
}

// SetActiveContext sets a context as active and deactivates all others for the user
func (r *KubeconfigRepo) SetActiveContext(ctx context.Context, userID, contextID int) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// First, verify the context exists and belongs to the user
var kubeContext models.KubeContext
if err := tx.Where("id = ? AND user_id = ?", contextID, userID).First(&kubeContext).Error; err != nil {
return err
}

// Deactivate all contexts for this user
if err := tx.Model(&models.KubeContext{}).
Where("user_id = ?", userID).
Update("is_active", false).Error; err != nil {
return err
}

// Activate the specified context
return tx.Model(&models.KubeContext{}).
Where("id = ? AND user_id = ?", contextID, userID).
Update("is_active", true).Error
})
}

// DeleteContext deletes a kubeconfig context
func (r *KubeconfigRepo) DeleteContext(ctx context.Context, userID, contextID int) error {
result := r.db.WithContext(ctx).
Where("id = ? AND user_id = ?", contextID, userID).
Delete(&models.KubeContext{})

if result.Error != nil {
return result.Error
}

if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}

return nil
}

// GetActiveContext retrieves the active kubeconfig context for a user
func (r *KubeconfigRepo) GetActiveContext(ctx context.Context, userID int) (*models.KubeContext, error) {
var kubeContext models.KubeContext
err := r.db.WithContext(ctx).
Where("user_id = ? AND is_active = ?", userID, true).
First(&kubeContext).Error
if err != nil {
return nil, err
}

// Decrypt sensitive fields
if err := r.decryptContext(&kubeContext); err != nil {
return nil, err
}

return &kubeContext, nil
}

// decryptContext decrypts all sensitive fields in a KubeContext
func (r *KubeconfigRepo) decryptContext(kubeContext *models.KubeContext) error {
var err error

if kubeContext.Token != "" {
kubeContext.Token, err = r.encryptor.Decrypt(kubeContext.Token)
if err != nil {
return fmt.Errorf("failed to decrypt token: %w", err)
}
}

if kubeContext.ClientCertificate != "" {
kubeContext.ClientCertificate, err = r.encryptor.Decrypt(kubeContext.ClientCertificate)
if err != nil {
return fmt.Errorf("failed to decrypt client certificate: %w", err)
}
}

if kubeContext.ClientKey != "" {
kubeContext.ClientKey, err = r.encryptor.Decrypt(kubeContext.ClientKey)
if err != nil {
return fmt.Errorf("failed to decrypt client key: %w", err)
}
}

if kubeContext.CertificateAuthorityData != "" {
kubeContext.CertificateAuthorityData, err = r.encryptor.Decrypt(kubeContext.CertificateAuthorityData)
if err != nil {
return fmt.Errorf("failed to decrypt CA data: %w", err)
}
}

return nil
}
Loading
Loading