421 lines
10 KiB
Go
421 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/mrhid6/keymanager/server/internal/auth"
|
|
"github.com/mrhid6/keymanager/server/internal/models"
|
|
"github.com/mrhid6/keymanager/server/internal/services"
|
|
)
|
|
|
|
func RegisterRoutes(r *gin.Engine) {
|
|
r.GET("/install", handleInstallScript)
|
|
r.GET("/update", handleUpdateScript)
|
|
|
|
// Auth endpoints (no session required)
|
|
r.GET("/auth/login", auth.HandleLogin)
|
|
r.GET("/auth/callback", auth.HandleCallback)
|
|
r.GET("/auth/logout", auth.HandleLogout)
|
|
r.GET("/auth/me", auth.HandleMe)
|
|
|
|
// API endpoints protected by session middleware
|
|
apiGroup := r.Group("/api")
|
|
apiGroup.Use(auth.Middleware())
|
|
{
|
|
apiGroup.GET("/servers", listServers)
|
|
apiGroup.POST("/servers", createServer)
|
|
apiGroup.GET("/servers/new", newServer)
|
|
apiGroup.POST("/servers/new", newServer)
|
|
apiGroup.GET("/servers/:id", getServer)
|
|
apiGroup.DELETE("/servers/:id", deleteServer)
|
|
apiGroup.POST("/servers/:id/generate-key", generateKey)
|
|
|
|
apiGroup.GET("/keys", listKeys)
|
|
apiGroup.POST("/keys", createKey)
|
|
apiGroup.GET("/keys/:id", getKey)
|
|
apiGroup.GET("/keys/:id/private-key", getPrivateKey)
|
|
apiGroup.DELETE("/keys/:id", deleteKey)
|
|
apiGroup.POST("/keys/:id/assign", assignKey)
|
|
apiGroup.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 = "https://keymanager.example.com"
|
|
}
|
|
|
|
installCmd := fmt.Sprintf(
|
|
`curl -fsSL "%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) {
|
|
id := c.Param("id")
|
|
|
|
var body struct {
|
|
Label string `json:"label"`
|
|
KeyType string `json:"key_type"`
|
|
KeySize int `json:"key_size"`
|
|
Passphrase string `json:"passphrase"`
|
|
Comment string `json:"comment"`
|
|
}
|
|
_ = c.ShouldBindJSON(&body)
|
|
if body.Label == "" {
|
|
body.Label = "generated"
|
|
}
|
|
|
|
s, err := services.GetServer(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "server not found"})
|
|
return
|
|
}
|
|
|
|
cmdID, err := services.DispatchGenerateKey(s.ServerID, services.KeyGenParams{
|
|
Label: body.Label,
|
|
KeyType: body.KeyType,
|
|
KeySize: body.KeySize,
|
|
Passphrase: body.Passphrase,
|
|
Comment: body.Comment,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{
|
|
"message": "key generation command sent to agent",
|
|
"command_id": cmdID,
|
|
"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"`
|
|
PrivateKey string `json:"private_key"`
|
|
}
|
|
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", "", body.PrivateKey)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusCreated, key)
|
|
}
|
|
|
|
func getPrivateKey(c *gin.Context) {
|
|
id := c.Param("id")
|
|
plaintext, err := services.GetPrivateKey(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"private_key": plaintext})
|
|
}
|
|
|
|
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)
|
|
|
|
type keyResponse struct {
|
|
*models.Key
|
|
Assignments any `json:"assignments"`
|
|
}
|
|
c.JSON(http.StatusOK, keyResponse{
|
|
Key: key,
|
|
Assignments: assignments,
|
|
})
|
|
}
|
|
|
|
func deleteKey(c *gin.Context) {
|
|
id := c.Param("id")
|
|
if err := services.DeleteKey(id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"deleted": true})
|
|
}
|
|
|
|
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 handleUpdateScript(c *gin.Context) {
|
|
giteaHost := os.Getenv("GITEA_HOST")
|
|
if giteaHost == "" {
|
|
giteaHost = "gitea.example.com"
|
|
}
|
|
|
|
script := fmt.Sprintf(`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
GITEA_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/}"
|
|
LATEST_ENCODED="${LATEST/\//%%2F}"
|
|
BINARY_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/keymanager-agent-linux-${ARCH}"
|
|
CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/checksums.txt"
|
|
|
|
echo "Updating keymanager-agent to ${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
|
|
|
|
systemctl stop keymanager-agent || true
|
|
install -m 0755 /tmp/keymanager-agent /usr/local/bin/keymanager-agent
|
|
systemctl start keymanager-agent
|
|
|
|
echo "keymanager-agent updated to ${VERSION} and restarted."
|
|
`, giteaHost)
|
|
|
|
c.Header("Content-Type", "text/x-shellscript")
|
|
c.String(http.StatusOK, script)
|
|
}
|
|
|
|
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"
|
|
}
|
|
grpcHost := os.Getenv("GRPC_HOST")
|
|
if grpcHost == "" {
|
|
grpcHost = publicHost
|
|
}
|
|
|
|
script := fmt.Sprintf(`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SERVER_ID="%s"
|
|
TOKEN="%s"
|
|
GITEA_HOST="%s"
|
|
KM_HOST="%s"
|
|
KM_HOST="${KM_HOST#https://}"
|
|
KM_HOST="${KM_HOST#http://}"
|
|
GRPC_HOST="%s"
|
|
GRPC_HOST="${GRPC_HOST#https://}"
|
|
GRPC_HOST="${GRPC_HOST#http://}"
|
|
|
|
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/}"
|
|
LATEST_ENCODED="${LATEST/\//%%2F}"
|
|
BINARY_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/keymanager-agent-linux-${ARCH}"
|
|
CHECKSUM_URL="https://${GITEA_HOST}/mrhid6/keymanager/releases/download/${LATEST_ENCODED}/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: "${GRPC_HOST}"
|
|
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, grpcHost)
|
|
|
|
c.Header("Content-Type", "text/x-shellscript")
|
|
c.String(http.StatusOK, script)
|
|
}
|