Core implementation written with Junie.

This commit is contained in:
2025-06-06 10:15:49 -07:00
parent 0ef669352f
commit e22c12fd39
28 changed files with 2597 additions and 24 deletions

110
cmd/mcias/database.go Normal file
View File

@@ -0,0 +1,110 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
dbUsername string
dbToken string
)
type DatabaseCredentials struct {
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
User string `json:"user"`
Password string `json:"password"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
var databaseCmd = &cobra.Command{
Use: "database",
Short: "Manage database credentials",
Long: `Commands for managing database credentials in the MCIAS system.`,
}
var getCredentialsCmd = &cobra.Command{
Use: "credentials",
Short: "Get database credentials",
Long: `Retrieve database credentials from the MCIAS system.
This command requires authentication with a username and token.`,
Run: func(cmd *cobra.Command, args []string) {
getCredentials()
},
}
func init() {
rootCmd.AddCommand(databaseCmd)
databaseCmd.AddCommand(getCredentialsCmd)
getCredentialsCmd.Flags().StringVarP(&dbUsername, "username", "u", "", "Username for authentication")
getCredentialsCmd.Flags().StringVarP(&dbToken, "token", "t", "", "Authentication token")
if err := getCredentialsCmd.MarkFlagRequired("username"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
}
if err := getCredentialsCmd.MarkFlagRequired("token"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking token flag as required: %v\n", err)
}
}
func getCredentials() {
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
serverAddr := viper.GetString("server")
if serverAddr == "" {
serverAddr = "http://localhost:8080"
}
url := fmt.Sprintf("%s/v1/database/credentials?username=%s", serverAddr, dbUsername)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
logger.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", dbToken))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Fatalf("Failed to send request: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logger.Fatalf("Failed to read response: %v", err)
}
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil {
logger.Fatalf("Error: %s", errResp.Error)
} else {
logger.Fatalf("Error: %s", resp.Status)
}
}
var creds DatabaseCredentials
if err := json.Unmarshal(body, &creds); err != nil {
logger.Fatalf("Failed to parse response: %v", err)
}
fmt.Println("Database Credentials:")
fmt.Printf("Host: %s\n", creds.Host)
fmt.Printf("Port: %d\n", creds.Port)
fmt.Printf("Name: %s\n", creds.Name)
fmt.Printf("User: %s\n", creds.User)
fmt.Printf("Password: %s\n", creds.Password)
}

68
cmd/mcias/init.go Normal file
View File

@@ -0,0 +1,68 @@
package main
import (
"database/sql"
"log"
"os"
"git.wntrmute.dev/kyle/mcias/database"
_ "github.com/mattn/go-sqlite3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
schemaFile string
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize the MCIAS database",
Long: `Initialize the MCIAS database with the default schema or a custom schema file.
This command will create a new database file if it doesn't exist,
or initialize an existing database.`,
Run: func(cmd *cobra.Command, args []string) {
initializeDatabase()
},
}
func init() {
rootCmd.AddCommand(initCmd)
initCmd.Flags().StringVarP(&schemaFile, "schema", "s", "", "Path to a custom schema file (default: embedded schema)")
}
func initializeDatabase() {
dbPath := viper.GetString("db")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
var schemaSQL string
if schemaFile != "" {
// Use custom schema file if provided
schemaBytes, err := os.ReadFile(schemaFile)
if err != nil {
logger.Fatalf("Failed to read custom schema file: %v", err)
}
schemaSQL = string(schemaBytes)
} else {
// Use embedded default schema
var err error
schemaSQL, err = database.DefaultSchema()
if err != nil {
logger.Fatalf("Failed to load default schema: %v", err)
}
}
_, err = db.Exec(schemaSQL)
if err != nil {
logger.Fatalf("Failed to initialize database: %v", err)
}
logger.Println("Database initialized successfully")
}

13
cmd/mcias/main.go Normal file
View File

@@ -0,0 +1,13 @@
package main
import (
"fmt"
"os"
)
func main() {
if err := Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

64
cmd/mcias/root.go Normal file
View File

@@ -0,0 +1,64 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
dbPath string
addr string
rootCmd = &cobra.Command{
Use: "mcias",
Short: "MCIAS - Metacircular Identity and Access System",
Long: `MCIAS is the metacircular identity and access system,
providing identity and authentication across metacircular projects.
It currently provides the following across metacircular services:
1. User password authentication
2. User token authentication
3. Database credential authentication`,
}
)
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mcias.yaml)")
rootCmd.PersistentFlags().StringVar(&dbPath, "db", "mcias.db", "Path to SQLite database file")
rootCmd.PersistentFlags().StringVar(&addr, "addr", ":8080", "Address to listen on")
if err := viper.BindPFlag("db", rootCmd.PersistentFlags().Lookup("db")); err != nil {
fmt.Fprintf(os.Stderr, "Error binding db flag: %v\n", err)
}
if err := viper.BindPFlag("addr", rootCmd.PersistentFlags().Lookup("addr")); err != nil {
fmt.Fprintf(os.Stderr, "Error binding addr flag: %v\n", err)
}
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".mcias")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}

78
cmd/mcias/root_test.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"testing"
"github.com/spf13/cobra"
)
func TestRootCommand(t *testing.T) {
if rootCmd.Use != "mcias" {
t.Errorf("Expected root command Use to be 'mcias', got '%s'", rootCmd.Use)
}
if rootCmd.Short == "" {
t.Error("Expected root command Short to be set")
}
if rootCmd.Long == "" {
t.Error("Expected root command Long to be set")
}
dbFlag := rootCmd.PersistentFlags().Lookup("db")
if dbFlag == nil {
t.Error("Expected 'db' flag to be defined")
} else {
if dbFlag.DefValue != "mcias.db" {
t.Errorf("Expected 'db' flag default value to be 'mcias.db', got '%s'", dbFlag.DefValue)
}
}
addrFlag := rootCmd.PersistentFlags().Lookup("addr")
if addrFlag == nil {
t.Error("Expected 'addr' flag to be defined")
} else {
if addrFlag.DefValue != ":8080" {
t.Errorf("Expected 'addr' flag default value to be ':8080', got '%s'", addrFlag.DefValue)
}
}
hasServerCmd := false
hasInitCmd := false
hasUserCmd := false
hasTokenCmd := false
for _, cmd := range rootCmd.Commands() {
switch cmd.Use {
case "server":
hasServerCmd = true
case "init":
hasInitCmd = true
case "user":
hasUserCmd = true
case "token":
hasTokenCmd = true
}
}
if !hasServerCmd {
t.Error("Expected 'server' command to be added to root command")
}
if !hasInitCmd {
t.Error("Expected 'init' command to be added to root command")
}
if !hasUserCmd {
t.Error("Expected 'user' command to be added to root command")
}
if !hasTokenCmd {
t.Error("Expected 'token' command to be added to root command")
}
}
func TestExecute(t *testing.T) {
origCmd := rootCmd
defer func() { rootCmd = origCmd }()
rootCmd = &cobra.Command{Use: "test"}
if err := Execute(); err != nil {
t.Errorf("Execute() returned an error: %v", err)
}
}

46
cmd/mcias/server.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
"database/sql"
"log"
"os"
"git.wntrmute.dev/kyle/mcias/api"
_ "github.com/mattn/go-sqlite3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var serverCmd = &cobra.Command{
Use: "server",
Short: "Start the MCIAS server",
Long: `Start the MCIAS server which provides authentication services.
The server will listen on the specified address and connect to the
specified database.`,
Run: func(cmd *cobra.Command, args []string) {
runServer()
},
}
func init() {
rootCmd.AddCommand(serverCmd)
}
func runServer() {
dbPath := viper.GetString("db")
addr := viper.GetString("addr")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
server := api.NewServer(db, logger)
logger.Printf("Starting MCIAS server on %s", addr)
if err := server.Start(addr); err != nil {
logger.Fatalf("Server error: %v", err)
}
}

144
cmd/mcias/token.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/oklog/ulid/v2"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
tokenUsername string
tokenDuration int64
)
var tokenCmd = &cobra.Command{
Use: "token",
Short: "Manage tokens",
Long: `Commands for managing authentication tokens in the MCIAS system.`,
}
var addTokenCmd = &cobra.Command{
Use: "add",
Short: "Add a new token for a user",
Long: `Add a new authentication token for a user in the MCIAS system.
This command requires a username and optionally a duration in hours.`,
Run: func(cmd *cobra.Command, args []string) {
addToken()
},
}
var listTokensCmd = &cobra.Command{
Use: "list",
Short: "List all tokens",
Long: `List all authentication tokens in the MCIAS system.`,
Run: func(cmd *cobra.Command, args []string) {
listTokens()
},
}
func init() {
rootCmd.AddCommand(tokenCmd)
tokenCmd.AddCommand(addTokenCmd)
tokenCmd.AddCommand(listTokensCmd)
addTokenCmd.Flags().StringVarP(&tokenUsername, "username", "u", "", "Username to create token for")
addTokenCmd.Flags().Int64VarP(&tokenDuration, "duration", "d", 24, "Token duration in hours (default 24)")
if err := addTokenCmd.MarkFlagRequired("username"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
}
}
func addToken() {
dbPath := viper.GetString("db")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
var userID string
err = db.QueryRow("SELECT id FROM users WHERE user = ?", tokenUsername).Scan(&userID)
if err != nil {
if err == sql.ErrNoRows {
logger.Fatalf("User %s does not exist", tokenUsername)
}
logger.Fatalf("Failed to check if user exists: %v", err)
}
token := ulid.Make().String()
expires := time.Now().Add(time.Duration(tokenDuration) * time.Hour).Unix()
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
tokenID := ulid.Make().String()
_, err = db.Exec(query, tokenID, userID, token, expires)
if err != nil {
logger.Fatalf("Failed to insert token into database: %v", err)
}
expiresTime := time.Unix(expires, 0).Format(time.RFC3339)
fmt.Printf("Token created successfully for user %s\n", tokenUsername)
fmt.Printf("Token: %s\n", token)
fmt.Printf("Expires: %s\n", expiresTime)
}
func listTokens() {
dbPath := viper.GetString("db")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
query := `
SELECT t.id, t.token, t.expires, u.user
FROM tokens t
JOIN users u ON t.uid = u.id
ORDER BY t.expires DESC
`
rows, err := db.Query(query)
if err != nil {
logger.Fatalf("Failed to query tokens: %v", err)
}
defer rows.Close()
fmt.Printf("%-24s %-30s %-20s %-20s %-10s\n", "ID", "TOKEN", "USERNAME", "EXPIRES", "STATUS")
fmt.Println(strings.Repeat("-", 100))
now := time.Now().Unix()
for rows.Next() {
var id, token, username string
var expires int64
if err := rows.Scan(&id, &token, &expires, &username); err != nil {
logger.Fatalf("Failed to scan token row: %v", err)
}
expiresTime := time.Unix(expires, 0).Format(time.RFC3339)
status := "ACTIVE"
if expires > 0 && expires < now {
status = "EXPIRED"
}
fmt.Printf("%-24s %-30s %-20s %-20s %-10s\n", id, token, username, expiresTime, status)
}
if err := rows.Err(); err != nil {
logger.Fatalf("Error iterating token rows: %v", err)
}
}

134
cmd/mcias/user.go Normal file
View File

@@ -0,0 +1,134 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"strings"
"time"
"git.wntrmute.dev/kyle/mcias/data"
_ "github.com/mattn/go-sqlite3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
username string
password string
)
var userCmd = &cobra.Command{
Use: "user",
Short: "Manage users",
Long: `Commands for managing users in the MCIAS system.`,
}
var addUserCmd = &cobra.Command{
Use: "add",
Short: "Add a new user",
Long: `Add a new user to the MCIAS system.
This command requires a username and password.`,
Run: func(cmd *cobra.Command, args []string) {
addUser()
},
}
var listUsersCmd = &cobra.Command{
Use: "list",
Short: "List all users",
Long: `List all users in the MCIAS system.`,
Run: func(cmd *cobra.Command, args []string) {
listUsers()
},
}
func init() {
rootCmd.AddCommand(userCmd)
userCmd.AddCommand(addUserCmd)
userCmd.AddCommand(listUsersCmd)
addUserCmd.Flags().StringVarP(&username, "username", "u", "", "Username for the new user")
addUserCmd.Flags().StringVarP(&password, "password", "p", "", "Password for the new user")
if err := addUserCmd.MarkFlagRequired("username"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
}
if err := addUserCmd.MarkFlagRequired("password"); err != nil {
fmt.Fprintf(os.Stderr, "Error marking password flag as required: %v\n", err)
}
}
func addUser() {
dbPath := viper.GetString("db")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
var count int
err = db.QueryRow("SELECT COUNT(*) FROM users WHERE user = ?", username).Scan(&count)
if err != nil {
logger.Fatalf("Failed to check if user exists: %v", err)
}
if count > 0 {
logger.Fatalf("User %s already exists", username)
}
user := &data.User{}
login := &data.Login{
User: username,
Password: password,
}
if err := user.Register(login); err != nil {
logger.Fatalf("Failed to register user: %v", err)
}
query := `INSERT INTO users (id, created, user, password, salt) VALUES (?, ?, ?, ?, ?)`
_, err = db.Exec(query, user.ID, user.Created, user.User, user.Password, user.Salt)
if err != nil {
logger.Fatalf("Failed to insert user into database: %v", err)
}
fmt.Printf("User %s added successfully with ID %s\n", user.User, user.ID)
}
func listUsers() {
dbPath := viper.GetString("db")
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
logger.Fatalf("Failed to open database: %v", err)
}
defer db.Close()
rows, err := db.Query("SELECT id, created, user FROM users ORDER BY user")
if err != nil {
logger.Fatalf("Failed to query users: %v", err)
}
defer rows.Close()
fmt.Printf("%-24s %-30s %-20s\n", "ID", "USERNAME", "CREATED")
fmt.Println(strings.Repeat("-", 76))
for rows.Next() {
var id string
var created int64
var username string
if err := rows.Scan(&id, &created, &username); err != nil {
logger.Fatalf("Failed to scan user row: %v", err)
}
createdTime := time.Unix(created, 0).Format(time.RFC3339)
fmt.Printf("%-24s %-30s %-20s\n", id, username, createdTime)
}
if err := rows.Err(); err != nil {
logger.Fatalf("Error iterating user rows: %v", err)
}
}