ip
This commit is contained in:
@@ -182,6 +182,28 @@ type CFTokenVerify struct {
|
|||||||
ExpiresOn string `json:"expires_on"`
|
ExpiresOn string `json:"expires_on"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CFTokenPolicy representa una política de permisos del token.
|
||||||
|
type CFTokenPolicy struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Effect string `json:"effect"`
|
||||||
|
Resources map[string]string `json:"resources"`
|
||||||
|
PermGroups []CFTokenPermGroup `json:"permission_groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFTokenPermGroup un grupo de permisos.
|
||||||
|
type CFTokenPermGroup struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CFTokenDetail detalle completo de un token incluyendo sus políticas.
|
||||||
|
type CFTokenDetail struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Policies []CFTokenPolicy `json:"policies"`
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyToken verifica la validez del API token usando /user/tokens/verify.
|
// VerifyToken verifica la validez del API token usando /user/tokens/verify.
|
||||||
func (c *CloudflareClient) VerifyToken() (*CFTokenVerify, error) {
|
func (c *CloudflareClient) VerifyToken() (*CFTokenVerify, error) {
|
||||||
var result CFTokenVerify
|
var result CFTokenVerify
|
||||||
@@ -191,6 +213,15 @@ func (c *CloudflareClient) VerifyToken() (*CFTokenVerify, error) {
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTokenDetail obtiene el detalle completo de un token (permisos y recursos).
|
||||||
|
func (c *CloudflareClient) GetTokenDetail(tokenID string) (*CFTokenDetail, error) {
|
||||||
|
var detail CFTokenDetail
|
||||||
|
if err := c.get("/user/tokens/"+tokenID, &detail); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetZones lista todas las zonas de la cuenta.
|
// GetZones lista todas las zonas de la cuenta.
|
||||||
func (c *CloudflareClient) GetZones() ([]CFZone, error) {
|
func (c *CloudflareClient) GetZones() ([]CFZone, error) {
|
||||||
var zones []CFZone
|
var zones []CFZone
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func cloudflareClient() (*services.CloudflareClient, error) {
|
|||||||
return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil
|
return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyCloudflareToken verifica que el token activo sea válido contra la API de Cloudflare.
|
// VerifyCloudflareToken verifica el token y devuelve sus permisos reales.
|
||||||
func VerifyCloudflareToken(c *fiber.Ctx) error {
|
func VerifyCloudflareToken(c *fiber.Ctx) error {
|
||||||
cfg, err := models.GetCloudflareConfig()
|
cfg, err := models.GetCloudflareConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,70 +116,77 @@ func VerifyCloudflareToken(c *fiber.Ctx) error {
|
|||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"})
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"})
|
||||||
}
|
}
|
||||||
dirty := len([]rune(raw)) != len([]rune(token))
|
dirty := len([]rune(raw)) != len([]rune(token))
|
||||||
client := services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
|
|
||||||
|
|
||||||
// Primer carácter y último para diagnóstico sin exponer el token completo
|
|
||||||
runes := []rune(token)
|
runes := []rune(token)
|
||||||
preview := ""
|
preview := ""
|
||||||
if len(runes) >= 8 {
|
if len(runes) >= 8 {
|
||||||
preview = string(runes[:4]) + "…" + string(runes[len(runes)-4:])
|
preview = string(runes[:4]) + "…" + string(runes[len(runes)-4:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Verificar que el token sea válido
|
client := services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
|
||||||
|
|
||||||
|
// 1. Verificar token
|
||||||
verify, err := client.VerifyToken()
|
verify, err := client.VerifyToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"ok": false,
|
"ok": false,
|
||||||
"step": "verify_token",
|
"step": "verify_token",
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
"hint": "El token no es válido. Crea uno nuevo en dash.cloudflare.com → Mi Perfil → API Tokens",
|
"hint": "Token inválido. Crea uno nuevo en dash.cloudflare.com → Mi Perfil → API Tokens",
|
||||||
"token_len": len(token),
|
"token_len": len(token),
|
||||||
"token_prev": preview,
|
"token_prev": preview,
|
||||||
"was_dirty": dirty,
|
"was_dirty": dirty,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Probar Zone:Zone:Read (listar zonas)
|
// 2. Obtener políticas reales del token
|
||||||
_, zonesErr := client.GetZones()
|
detail, detailErr := client.GetTokenDetail(verify.ID)
|
||||||
|
detailErrMsg := ""
|
||||||
|
if detailErr != nil {
|
||||||
|
detailErrMsg = detailErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Probar Zone:Zone:Read
|
||||||
|
zones, zonesErr := client.GetZones()
|
||||||
zonesOK := zonesErr == nil
|
zonesOK := zonesErr == nil
|
||||||
zonesErrMsg := ""
|
zonesErrMsg := ""
|
||||||
if zonesErr != nil {
|
if zonesErr != nil {
|
||||||
zonesErrMsg = zonesErr.Error()
|
zonesErrMsg = zonesErr.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Probar Zone:DNS:Read (leer DNS de la primera zona disponible)
|
// 4. Probar Zone:DNS:Read en la primera zona
|
||||||
dnsOK := false
|
dnsOK := false
|
||||||
dnsErrMsg := ""
|
dnsErrMsg := ""
|
||||||
if zonesOK {
|
dnsZoneID := ""
|
||||||
zones, _ := client.GetZones()
|
if zonesOK && len(zones) > 0 {
|
||||||
if len(zones) > 0 {
|
dnsZoneID = zones[0].ID
|
||||||
_, dnsErr := client.GetDNSRecords(zones[0].ID)
|
_, dnsErr := client.GetDNSRecords(dnsZoneID)
|
||||||
dnsOK = dnsErr == nil
|
dnsOK = dnsErr == nil
|
||||||
if dnsErr != nil {
|
if dnsErr != nil {
|
||||||
dnsErrMsg = dnsErr.Error()
|
dnsErrMsg = dnsErr.Error()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hint := "Token válido con todos los permisos necesarios"
|
||||||
|
if !zonesOK {
|
||||||
|
hint = "Falta permiso Zone:Zone:Read"
|
||||||
|
} else if !dnsOK {
|
||||||
|
hint = "Falta permiso Zone:DNS:Read — o el token tiene scope de zonas específicas y no incluye la zona consultada. En Cloudflare: edita el token → Zone Resources → cambia a 'All zones'."
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"token_len": len(token),
|
|
||||||
"token_prev": preview,
|
|
||||||
"was_dirty": dirty,
|
|
||||||
"token_status": verify.Status,
|
"token_status": verify.Status,
|
||||||
"permissions": fiber.Map{
|
"token_id": verify.ID,
|
||||||
"zone_read": fiber.Map{"ok": zonesOK, "error": zonesErrMsg},
|
"token_len": len(token),
|
||||||
"dns_read": fiber.Map{"ok": dnsOK, "error": dnsErrMsg},
|
"token_prev": preview,
|
||||||
|
"was_dirty": dirty,
|
||||||
|
"detail_error": detailErrMsg,
|
||||||
|
"policies": func() interface{} { if detail != nil { return detail.Policies }; return nil }(),
|
||||||
|
"permissions_test": fiber.Map{
|
||||||
|
"zone_read": fiber.Map{"ok": zonesOK, "error": zonesErrMsg},
|
||||||
|
"dns_read": fiber.Map{"ok": dnsOK, "error": dnsErrMsg, "zone_tested": dnsZoneID},
|
||||||
},
|
},
|
||||||
"hint": func() string {
|
"hint": hint,
|
||||||
if !zonesOK {
|
|
||||||
return "Falta permiso Zone:Zone:Read en el token"
|
|
||||||
}
|
|
||||||
if !dnsOK {
|
|
||||||
return "Falta permiso Zone:DNS:Read en el token. Edita el token en Cloudflare y agrega ese permiso."
|
|
||||||
}
|
|
||||||
return "Token válido con todos los permisos necesarios"
|
|
||||||
}(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user