mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Add `POST /admin/masters/{id}/keys` allowing admins to issue child keys
on behalf of a master. Introduce an `issued_by` field in the Key model
to audit whether a key was issued by the master or an admin.
Refactor master service to use typed errors for consistent HTTP status
mapping and ensure validation logic (active status, group check) is
shared.
97 lines
2.9 KiB
Go
97 lines
2.9 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type MasterHandler struct {
|
|
masterService *service.MasterService
|
|
syncService *service.SyncService
|
|
}
|
|
|
|
func NewMasterHandler(masterService *service.MasterService, syncService *service.SyncService) *MasterHandler {
|
|
return &MasterHandler{masterService: masterService, syncService: syncService}
|
|
}
|
|
|
|
type IssueChildKeyRequest struct {
|
|
Group string `json:"group"`
|
|
Scopes string `json:"scopes"`
|
|
}
|
|
|
|
// IssueChildKey godoc
|
|
// @Summary Issue a child key
|
|
// @Description Issue a new access token (child key) for the authenticated master
|
|
// @Tags master
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security MasterAuth
|
|
// @Param request body IssueChildKeyRequest true "Key Request"
|
|
// @Success 201 {object} gin.H
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 401 {object} gin.H
|
|
// @Failure 403 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /v1/tokens [post]
|
|
func (h *MasterHandler) IssueChildKey(c *gin.Context) {
|
|
master, exists := c.Get("master")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "master key not found in context"})
|
|
return
|
|
}
|
|
masterModel := master.(*model.Master)
|
|
|
|
var req IssueChildKeyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// If group is not specified, inherit from master
|
|
group := req.Group
|
|
if strings.TrimSpace(group) == "" {
|
|
group = masterModel.Group
|
|
}
|
|
|
|
// Security: Ensure the requested group is allowed for this master.
|
|
// For now, we'll just enforce it's the same group.
|
|
if group != masterModel.Group {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "cannot issue key for a different group"})
|
|
return
|
|
}
|
|
|
|
key, rawChildKey, err := h.masterService.IssueChildKey(masterModel.ID, group, req.Scopes)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, service.ErrMasterNotFound):
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
case errors.Is(err, service.ErrMasterNotActive):
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
case errors.Is(err, service.ErrChildKeyGroupForbidden):
|
|
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
case errors.Is(err, service.ErrChildKeyLimitReached):
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
default:
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to issue child key", "details": err.Error()})
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := h.syncService.SyncKey(key); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to sync child key", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"id": key.ID,
|
|
"key_secret": rawChildKey,
|
|
"group": key.Group,
|
|
"scopes": key.Scopes,
|
|
})
|
|
}
|