Files
keymanager/server/internal/api/handlers.go
T
domrichardson c9868b2108
Agent Release / build (push) Has been cancelled
Server Deploy / deploy (push) Has been cancelled
first commit
2026-06-15 13:58:45 +01:00

294 lines
7.0 KiB
Go

package api
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/mrhid6/keymanager/server/internal/models"
"github.com/mrhid6/keymanager/server/internal/services"
)
func RegisterRoutes(r *gin.Engine) {
r.GET("/install", handleInstallScript)
api := r.Group("/api")
{
api.GET("/servers", listServers)
api.POST("/servers", createServer)
api.GET("/servers/new", newServer)
api.POST("/servers/new", newServer)
api.GET("/servers/:id", getServer)
api.DELETE("/servers/:id", deleteServer)
api.POST("/servers/:id/generate-key", generateKey)
api.GET("/keys", listKeys)
api.POST("/keys", createKey)
api.GET("/keys/:id", getKey)
api.POST("/keys/:id/assign", assignKey)
api.DELETE("/keys/:id/assign/:serverId", revokeAssignment)
}
}
func listServers(c *gin.Context) {
servers, err := services.ListServers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, servers)
}
func createServer(c *gin.Context) {
s, token, err := services.CreateServer()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"server": s,
"token": token,
"server_id": s.ServerID,
})
}
func newServer(c *gin.Context) {
s, token, err := services.CreateServer()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
giteaHost := os.Getenv("GITEA_HOST")
if giteaHost == "" {
giteaHost = "gitea.example.com"
}
host := os.Getenv("PUBLIC_HOST")
if host == "" {
host = "keymanager.example.com"
}
installCmd := fmt.Sprintf(
`curl -fsSL "https://%s/install?server_id=%s&token=%s" | bash`,
host, s.ServerID, token,
)
c.JSON(http.StatusOK, gin.H{
"server_id": s.ServerID,
"pre_reg_token": token,
"install_command": installCmd,
})
}
func getServer(c *gin.Context) {
id := c.Param("id")
s, err := services.GetServer(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
return
}
assignments, _ := services.GetAssignmentsWithKeysForServer(id)
// Build response matching ServerWithKeys shape expected by frontend
type serverResponse struct {
*models.Server
Keys interface{} `json:"keys"`
}
c.JSON(http.StatusOK, serverResponse{
Server: s,
Keys: assignments,
})
}
func deleteServer(c *gin.Context) {
id := c.Param("id")
if err := services.DeleteServer(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deleted": true})
}
func generateKey(c *gin.Context) {
// The agent triggers key generation itself; this endpoint signals
// the intent by returning the server so the caller knows to wait
// for the agent to upload via gRPC UploadGeneratedKey.
id := c.Param("id")
s, err := services.GetServer(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "agent will generate and upload key on next poll",
"server_id": s.ServerID,
})
}
func listKeys(c *gin.Context) {
keys, err := services.ListKeys()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, keys)
}
func createKey(c *gin.Context) {
var body struct {
Label string `json:"label" binding:"required"`
PublicKey string `json:"public_key" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
key, err := services.CreateKey(body.Label, body.PublicKey, "uploaded", "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, key)
}
func getKey(c *gin.Context) {
id := c.Param("id")
key, err := services.GetKey(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "key not found"})
return
}
assignments, _ := services.GetAssignmentsWithServers(id)
c.JSON(http.StatusOK, gin.H{
"key": key,
"assignments": assignments,
})
}
func assignKey(c *gin.Context) {
keyID := c.Param("id")
var body struct {
ServerID string `json:"server_id" binding:"required"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
a, err := services.AssignKey(keyID, body.ServerID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, a)
}
func revokeAssignment(c *gin.Context) {
keyID := c.Param("id")
serverID := c.Param("serverId")
if err := services.RevokeAssignment(keyID, serverID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"revoked": true})
}
func handleInstallScript(c *gin.Context) {
serverID := c.Query("server_id")
token := c.Query("token")
giteaHost := os.Getenv("GITEA_HOST")
if giteaHost == "" {
giteaHost = "gitea.example.com"
}
publicHost := os.Getenv("PUBLIC_HOST")
if publicHost == "" {
publicHost = "keymanager.example.com"
}
script := fmt.Sprintf(`#!/usr/bin/env bash
set -euo pipefail
SERVER_ID="%s"
TOKEN="%s"
GITEA_HOST="%s"
KM_HOST="%s"
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64) ARCH="arm64" ;;
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
esac
# Get latest agent release tag
LATEST=$(curl -fsSL "https://${GITEA_HOST}/api/v1/repos/mrhid6/keymanager/releases?limit=10" \
| grep -o '"tag_name":"agent/v[^"]*"' | head -1 | sed 's/"tag_name":"//;s/"//')
if [ -z "$LATEST" ]; then
echo "Could not determine latest agent version" >&2
exit 1
fi
VERSION="${LATEST#agent/}"
BINARY_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST}/keymanager-agent-linux-${ARCH}"
CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST}/checksums.txt"
echo "Installing keymanager-agent ${VERSION} (${ARCH})..."
curl -fsSL -o /tmp/keymanager-agent "${BINARY_URL}"
curl -fsSL -o /tmp/checksums.txt "${CHECKSUM_URL}"
cd /tmp
EXPECTED=$(grep "keymanager-agent-linux-${ARCH}" checksums.txt | awk '{print $1}')
ACTUAL=$(sha256sum keymanager-agent | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "Checksum mismatch!" >&2
exit 1
fi
install -m 0755 /tmp/keymanager-agent /usr/local/bin/keymanager-agent
mkdir -p /etc/keymanager
chmod 0700 /etc/keymanager
cat > /etc/keymanager/config.yaml <<EOF
server_url: "${KM_HOST}:9090"
server_id: "${SERVER_ID}"
pre_reg_token: "${TOKEN}"
agent_token: ""
poll_interval: 30s
tls: true
EOF
chmod 0600 /etc/keymanager/config.yaml
cat > /etc/systemd/system/keymanager-agent.service <<EOF
[Unit]
Description=KeyManager Agent
After=network.target
[Service]
ExecStart=/usr/local/bin/keymanager-agent
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now keymanager-agent
echo "keymanager-agent installed and started."
`, serverID, token, giteaHost, publicHost)
c.Header("Content-Type", "text/x-shellscript")
c.String(http.StatusOK, script)
}