first commit
Agent Release / build (push) Has been cancelled
Server Deploy / deploy (push) Has been cancelled

This commit is contained in:
domrichardson
2026-06-15 13:58:45 +01:00
commit c9868b2108
55 changed files with 11076 additions and 0 deletions
+209
View File
@@ -0,0 +1,209 @@
package services
import (
"context"
"crypto/md5"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/mrhid6/keymanager/server/internal/db"
"github.com/mrhid6/keymanager/server/internal/models"
"go.mongodb.org/mongo-driver/v2/bson"
)
func computeFingerprint(pubKey string) string {
parts := strings.Fields(pubKey)
if len(parts) < 2 {
return ""
}
raw, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return ""
}
sum := md5.Sum(raw)
var pairs []string
for _, b := range sum {
pairs = append(pairs, fmt.Sprintf("%02x", b))
}
return "MD5:" + strings.Join(pairs, ":")
}
func CreateKey(label, publicKey, source, generatedByServerID string) (*models.Key, error) {
key := &models.Key{
KeyID: uuid.NewString(),
Label: label,
PublicKey: publicKey,
Fingerprint: computeFingerprint(publicKey),
Source: source,
GeneratedByServerID: generatedByServerID,
CreatedAt: time.Now(),
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.Col("keys").InsertOne(ctx, key)
if err != nil {
return nil, err
}
return key, nil
}
func GetKey(keyID string) (*models.Key, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var key models.Key
err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID}).Decode(&key)
if err != nil {
return nil, err
}
return &key, nil
}
func ListKeys() ([]models.Key, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := db.Col("keys").Find(ctx, bson.M{})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var keys []models.Key
if err := cursor.All(ctx, &keys); err != nil {
return nil, err
}
return keys, nil
}
func DeleteKey(keyID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.Col("keys").DeleteOne(ctx, bson.M{"key_id": keyID})
return err
}
func AssignKey(keyID, serverID string) (*models.Assignment, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Check if already assigned and active
var existing models.Assignment
err := db.Col("assignments").FindOne(ctx, bson.M{
"key_id": keyID,
"server_id": serverID,
"revoked_at": nil,
}).Decode(&existing)
if err == nil {
return &existing, nil
}
a := &models.Assignment{
KeyID: keyID,
ServerID: serverID,
AssignedAt: time.Now(),
}
_, err = db.Col("assignments").InsertOne(ctx, a)
if err != nil {
return nil, err
}
return a, nil
}
func RevokeAssignment(keyID, serverID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
now := time.Now()
_, err := db.Col("assignments").UpdateOne(ctx,
bson.M{"key_id": keyID, "server_id": serverID, "revoked_at": nil},
bson.M{"$set": bson.M{"revoked_at": now}},
)
return err
}
func GetAssignmentsForKey(keyID string) ([]models.Assignment, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := db.Col("assignments").Find(ctx, bson.M{"key_id": keyID, "revoked_at": nil})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var assignments []models.Assignment
if err := cursor.All(ctx, &assignments); err != nil {
return nil, err
}
return assignments, nil
}
type AssignmentWithServer struct {
models.Assignment `bson:",inline"`
Server *models.Server `json:"server,omitempty"`
}
func GetAssignmentsWithServers(keyID string) ([]AssignmentWithServer, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := db.Col("assignments").Find(ctx, bson.M{"key_id": keyID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var assignments []models.Assignment
if err := cursor.All(ctx, &assignments); err != nil {
return nil, err
}
result := make([]AssignmentWithServer, 0, len(assignments))
for _, a := range assignments {
item := AssignmentWithServer{Assignment: a}
var srv models.Server
if err := db.Col("servers").FindOne(ctx, bson.M{"server_id": a.ServerID}).Decode(&srv); err == nil {
item.Server = &srv
}
result = append(result, item)
}
return result, nil
}
type AssignmentWithKey struct {
models.Assignment `bson:",inline"`
Key *models.Key `json:"key,omitempty"`
}
func GetAssignmentsWithKeysForServer(serverID string) ([]AssignmentWithKey, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := db.Col("assignments").Find(ctx, bson.M{"server_id": serverID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var assignments []models.Assignment
if err := cursor.All(ctx, &assignments); err != nil {
return nil, err
}
result := make([]AssignmentWithKey, 0, len(assignments))
for _, a := range assignments {
item := AssignmentWithKey{Assignment: a}
var key models.Key
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID}).Decode(&key); err == nil {
item.Key = &key
}
result = append(result, item)
}
return result, nil
}
+193
View File
@@ -0,0 +1,193 @@
package services
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/google/uuid"
"github.com/mrhid6/keymanager/server/internal/db"
"github.com/mrhid6/keymanager/server/internal/models"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
func generateToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func HashToken(token string) string {
sum := sha256.Sum256([]byte(token))
return hex.EncodeToString(sum[:])
}
func CreateServer() (*models.Server, string, error) {
token, err := generateToken(32)
if err != nil {
return nil, "", err
}
expires := time.Now().Add(time.Hour)
s := &models.Server{
ServerID: uuid.NewString(),
PreRegToken: token,
PreRegExpires: &expires,
Status: "pending",
CreatedAt: time.Now(),
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = db.Col("servers").InsertOne(ctx, s)
if err != nil {
return nil, "", err
}
return s, token, nil
}
func GetServer(serverID string) (*models.Server, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var s models.Server
err := db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID}).Decode(&s)
if err != nil {
return nil, err
}
return &s, nil
}
func GetServerByPreRegToken(token string) (*models.Server, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var s models.Server
err := db.Col("servers").FindOne(ctx, bson.M{
"pre_reg_token": token,
"pre_reg_expires": bson.M{"$gt": time.Now()},
}).Decode(&s)
if err != nil {
return nil, err
}
return &s, nil
}
func RegisterServer(serverID, preRegToken, hostname, ipAddress, osInfo string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var s models.Server
err := db.Col("servers").FindOne(ctx, bson.M{
"server_id": serverID,
"pre_reg_token": preRegToken,
"pre_reg_expires": bson.M{"$gt": time.Now()},
}).Decode(&s)
if err != nil {
return "", fmt.Errorf("invalid or expired pre-registration token")
}
agentToken, err := generateToken(32)
if err != nil {
return "", err
}
tokenHash := HashToken(agentToken)
now := time.Now()
_, err = db.Col("servers").UpdateOne(ctx,
bson.M{"server_id": serverID},
bson.M{"$set": bson.M{
"hostname": hostname,
"ip_address": ipAddress,
"os_info": osInfo,
"agent_token_hash": tokenHash,
"status": "active",
"last_seen": now,
"pre_reg_token": "",
"pre_reg_expires": nil,
}},
)
if err != nil {
return "", err
}
return agentToken, nil
}
func ValidateAgentToken(serverID, agentToken string) (*models.Server, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tokenHash := HashToken(agentToken)
var s models.Server
err := db.Col("servers").FindOne(ctx, bson.M{
"server_id": serverID,
"agent_token_hash": tokenHash,
}).Decode(&s)
if err != nil {
return nil, fmt.Errorf("invalid agent token")
}
return &s, nil
}
func UpdateServerLastSeen(serverID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
now := time.Now()
_, err := db.Col("servers").UpdateOne(ctx,
bson.M{"server_id": serverID},
bson.M{"$set": bson.M{"last_seen": now, "status": "active"}},
)
return err
}
func ListServers() ([]models.Server, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := db.Col("servers").Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var servers []models.Server
if err := cursor.All(ctx, &servers); err != nil {
return nil, err
}
return servers, nil
}
func DeleteServer(serverID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := db.Col("servers").DeleteOne(ctx, bson.M{"server_id": serverID})
if err != nil {
return err
}
// Also remove assignments
_, err = db.Col("assignments").DeleteMany(ctx, bson.M{"server_id": serverID})
return err
}
func MarkOfflineServers(threshold time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cutoff := time.Now().Add(-threshold)
_, err := db.Col("servers").UpdateMany(ctx,
bson.M{
"status": "active",
"last_seen": bson.M{"$lt": cutoff},
},
bson.M{"$set": bson.M{"status": "offline"}},
)
return err
}
+40
View File
@@ -0,0 +1,40 @@
package services
import (
"context"
"time"
"github.com/mrhid6/keymanager/server/internal/db"
"github.com/mrhid6/keymanager/server/internal/models"
"go.mongodb.org/mongo-driver/v2/bson"
)
func BuildAuthorizedKeys(serverID string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := db.Col("assignments").Find(ctx, bson.M{
"server_id": serverID,
"revoked_at": nil,
})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var assignments []models.Assignment
if err := cursor.All(ctx, &assignments); err != nil {
return nil, err
}
var lines []string
for _, a := range assignments {
var key models.Key
err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID}).Decode(&key)
if err != nil {
continue
}
lines = append(lines, key.PublicKey)
}
return lines, nil
}