Core implementation written with Junie.
This commit is contained in:
parent
0ef669352f
commit
e22c12fd39
|
@ -0,0 +1,27 @@
|
|||
# Project Guidelines
|
||||
|
||||
MCIAS is the metacircular identity and access system, providing identity and
|
||||
authentication across the metacircular projects.
|
||||
|
||||
## Structure
|
||||
|
||||
+ The system should be runnable through a cobra CLI tool, with
|
||||
subcommands implemented as wrappers around packages.
|
||||
+ The REST API code should be under the `api` directory.
|
||||
+ The system should be backed by a single SQLite database whose schema
|
||||
is stored in `schema.sql`.
|
||||
|
||||
## Writing code
|
||||
|
||||
+ Junie should write and run tests to validate correctness.
|
||||
+ The tests should be runnable with just `go test` (with
|
||||
any approporiate arguments).
|
||||
+ Junie should validate the build and ensure that the code is
|
||||
properly linted. Junie should use `golangci-lint` for this.
|
||||
+ Junie should elide trivial comments, and only write comments where it
|
||||
is beneficial to provide exposition on the code.
|
||||
|
||||
## Notes
|
||||
|
||||
This is a security system, so care should be taken towards
|
||||
correctness.
|
99
README.org
99
README.org
|
@ -3,7 +3,7 @@
|
|||
|
||||
* MCIAS
|
||||
|
||||
MCIAS is the metacircular identity and access system.
|
||||
MCIAS is the metacircular identity and access system, providing identity and authentication across metacircular projects.
|
||||
|
||||
It currently provides the following across metacircular services:
|
||||
|
||||
|
@ -12,16 +12,93 @@
|
|||
3. Database credential authentication.
|
||||
|
||||
Future work should consider adding support for:
|
||||
1. TOTP
|
||||
2. Policy management.
|
||||
1. TOTP (Time-based One-Time Password)
|
||||
2. Policy management for fine-grained access control.
|
||||
|
||||
** API endpoints
|
||||
* Documentation
|
||||
|
||||
*** The login type
|
||||
Comprehensive documentation is available in the [[file:docs/][docs]] directory:
|
||||
|
||||
- [[file:docs/overview.org][Overview]] - Project overview, system architecture, database schema, and security considerations
|
||||
- [[file:docs/api.org][API Documentation]] - API endpoints, request/response formats, error handling, and authentication flow
|
||||
- [[file:docs/installation.org][Installation and Usage Guide]] - Prerequisites, installation steps, running the server, and more
|
||||
|
||||
* Quick Start
|
||||
|
||||
To get started with MCIAS:
|
||||
|
||||
1. Initialize the database:
|
||||
#+begin_src bash
|
||||
go run main.go init --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
2. Start the server:
|
||||
#+begin_src bash
|
||||
go run main.go server --db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
3. The server will listen on port 8080 by default.
|
||||
|
||||
* CLI Commands
|
||||
|
||||
MCIAS provides a command-line interface with the following commands:
|
||||
|
||||
** Server Command
|
||||
|
||||
Start the MCIAS server:
|
||||
#+begin_src bash
|
||||
go run main.go server [--db <path>] [--addr <address>]
|
||||
#+end_src
|
||||
|
||||
** Init Command
|
||||
|
||||
Initialize the database:
|
||||
#+begin_src bash
|
||||
go run main.go init [--db <path>]
|
||||
#+end_src
|
||||
|
||||
** User Commands
|
||||
|
||||
Add a new user:
|
||||
#+begin_src bash
|
||||
go run main.go user add --username <username> --password <password>
|
||||
#+end_src
|
||||
|
||||
List all users:
|
||||
#+begin_src bash
|
||||
go run main.go user list
|
||||
#+end_src
|
||||
|
||||
** Token Commands
|
||||
|
||||
Add a new token for a user:
|
||||
#+begin_src bash
|
||||
go run main.go token add --username <username> [--duration <hours>]
|
||||
#+end_src
|
||||
|
||||
List all tokens:
|
||||
#+begin_src bash
|
||||
go run main.go token list
|
||||
#+end_src
|
||||
|
||||
* API Overview
|
||||
|
||||
** Authentication Endpoints
|
||||
|
||||
*** =/v1/login/password=
|
||||
Password-based authentication endpoint.
|
||||
|
||||
*** =/v1/login/token=
|
||||
Token-based authentication endpoint.
|
||||
|
||||
*** =/v1/credentials/database=
|
||||
Database credential authentication endpoint (not yet fully implemented).
|
||||
|
||||
** Request Format
|
||||
|
||||
The general datastructure used to log in should look like:
|
||||
|
||||
#+begin_src: json
|
||||
#+begin_src json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
|
@ -37,11 +114,9 @@
|
|||
=login.user= types are required, as well as the appropriate
|
||||
credential field.
|
||||
|
||||
*** =/v1/login/password=
|
||||
* Development
|
||||
|
||||
The request should be a JSON object:
|
||||
- Run tests: =go test ./...=
|
||||
- Run linter: =golangci-lint run=
|
||||
|
||||
|
||||
*** =/v1/login/token=
|
||||
|
||||
*** =/v1/credentials/database=
|
||||
See the [[file:docs/installation.org][Installation and Usage Guide]] for more details.
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/data"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Version string `json:"version"`
|
||||
Login data.Login `json:"login"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
Expires int64 `json:"expires"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type DatabaseCredentials struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Name string `json:"name"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Version != "v1" || req.Login.User == "" || req.Login.Password == "" {
|
||||
s.sendError(w, "Invalid login request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.getUserByUsername(req.Login.User)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
s.sendError(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
} else {
|
||||
s.Logger.Printf("Database error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Check(&req.Login) {
|
||||
s.sendError(w, "Invalid username or password", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, expires, err := s.createToken(user.ID)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Token creation error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(TokenResponse{
|
||||
Token: token,
|
||||
Expires: expires,
|
||||
}); err != nil {
|
||||
s.Logger.Printf("Error encoding response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleTokenLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendError(w, "Invalid request format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Version != "v1" || req.Login.User == "" || req.Login.Token == "" {
|
||||
s.sendError(w, "Invalid login request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := s.verifyToken(req.Login.User, req.Login.Token)
|
||||
if err != nil {
|
||||
s.sendError(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token, expires, err := s.createToken(userID)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Token creation error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(TokenResponse{
|
||||
Token: token,
|
||||
Expires: expires,
|
||||
}); err != nil {
|
||||
s.Logger.Printf("Error encoding response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sendError(w http.ResponseWriter, message string, status int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(ErrorResponse{Error: message}); err != nil {
|
||||
s.Logger.Printf("Error encoding error response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) getUserByUsername(username string) (*data.User, error) {
|
||||
query := `SELECT id, created, user, password, salt FROM users WHERE user = ?`
|
||||
row := s.DB.QueryRow(query, username)
|
||||
|
||||
user := &data.User{}
|
||||
err := row.Scan(&user.ID, &user.Created, &user.User, &user.Password, &user.Salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rolesQuery := `
|
||||
SELECT r.role FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.rid
|
||||
WHERE ur.uid = ?
|
||||
`
|
||||
rows, err := s.DB.Query(rolesQuery, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var roles []string
|
||||
for rows.Next() {
|
||||
var role string
|
||||
if err := rows.Scan(&role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
user.Roles = roles
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Server) createToken(userID string) (string, int64, error) {
|
||||
token := ulid.Make().String()
|
||||
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
|
||||
tokenID := ulid.Make().String()
|
||||
_, err := s.DB.Exec(query, tokenID, userID, token, expires)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return token, expires, nil
|
||||
}
|
||||
|
||||
func (s *Server) verifyToken(username, token string) (string, error) {
|
||||
query := `
|
||||
SELECT t.uid, t.expires FROM tokens t
|
||||
JOIN users u ON t.uid = u.id
|
||||
WHERE u.user = ? AND t.token = ?
|
||||
`
|
||||
var userID string
|
||||
var expires int64
|
||||
err := s.DB.QueryRow(query, username, token).Scan(&userID, &expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if expires > 0 && expires < time.Now().Unix() {
|
||||
return "", errors.New("token expired")
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleDatabaseCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
s.sendError(w, "Authorization header required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a Bearer token
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
s.sendError(w, "Invalid authorization format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
username := r.URL.Query().Get("username")
|
||||
if username == "" {
|
||||
s.sendError(w, "Username parameter required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the token
|
||||
_, err := s.verifyToken(username, token)
|
||||
if err != nil {
|
||||
s.sendError(w, "Invalid or expired token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has admin role
|
||||
user, err := s.getUserByUsername(username)
|
||||
if err != nil {
|
||||
s.Logger.Printf("Database error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hasAdminRole := false
|
||||
for _, role := range user.Roles {
|
||||
if role == "admin" {
|
||||
hasAdminRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasAdminRole {
|
||||
s.sendError(w, "Insufficient permissions", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve database credentials
|
||||
query := `SELECT id, host, port, name, user, password FROM database LIMIT 1`
|
||||
row := s.DB.QueryRow(query)
|
||||
|
||||
var id string
|
||||
var creds DatabaseCredentials
|
||||
err = row.Scan(&id, &creds.Host, &creds.Port, &creds.Name, &creds.User, &creds.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
s.sendError(w, "No database credentials found", http.StatusNotFound)
|
||||
} else {
|
||||
s.Logger.Printf("Database error: %v", err)
|
||||
s.sendError(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return the credentials
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(creds); err != nil {
|
||||
s.Logger.Printf("Error encoding response: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/data"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open test database: %v", err)
|
||||
}
|
||||
|
||||
schema, err := os.ReadFile("../schema.sql")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read schema: %v", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(string(schema)); err != nil {
|
||||
t.Fatalf("Failed to initialize test database: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func createTestUser(t *testing.T, db *sql.DB) *data.User {
|
||||
user := &data.User{}
|
||||
login := &data.Login{
|
||||
User: "testuser",
|
||||
Password: "testpassword",
|
||||
}
|
||||
|
||||
if err := user.Register(login); err != nil {
|
||||
t.Fatalf("Failed to register test 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 {
|
||||
t.Fatalf("Failed to insert test user: %v", err)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func TestPasswordLogin(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestUser(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: data.Login{
|
||||
User: user.User,
|
||||
Password: "testpassword",
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/v1/login/password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handlePasswordLogin(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
var response TokenResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Token == "" {
|
||||
t.Error("Expected token in response, got empty string")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if response.Expires <= now {
|
||||
t.Errorf("Expected token expiration in the future, got %d (now: %d)", response.Expires, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenLogin(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestUser(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
|
||||
token := "testtoken123456"
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
tokenID := "token123"
|
||||
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
|
||||
_, err := db.Exec(query, tokenID, user.ID, token, expires)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert test token: %v", err)
|
||||
}
|
||||
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: data.Login{
|
||||
User: user.User,
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/v1/login/token", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handleTokenLogin(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
var response TokenResponse
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Token == "" {
|
||||
t.Error("Expected token in response, got empty string")
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if response.Expires <= now {
|
||||
t.Errorf("Expected token expiration in the future, got %d (now: %d)", response.Expires, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPasswordLogin(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestUser(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: data.Login{
|
||||
User: user.User,
|
||||
Password: "wrongpassword",
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/v1/login/password", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handlePasswordLogin(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusUnauthorized, recorder.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidTokenLogin(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestUser(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
|
||||
loginReq := LoginRequest{
|
||||
Version: "v1",
|
||||
Login: data.Login{
|
||||
User: user.User,
|
||||
Token: "invalidtoken",
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(loginReq)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("POST", "/v1/login/token", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handleTokenLogin(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusUnauthorized, recorder.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestAdminUser(t *testing.T, db *sql.DB) *data.User {
|
||||
user := createTestUser(t, db)
|
||||
|
||||
// Add admin role
|
||||
roleID := "role123"
|
||||
_, err := db.Exec("INSERT INTO roles (id, role) VALUES (?, ?)", roleID, "admin")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert admin role: %v", err)
|
||||
}
|
||||
|
||||
// Assign admin role to user
|
||||
userRoleID := "ur123"
|
||||
_, err = db.Exec("INSERT INTO user_roles (id, uid, rid) VALUES (?, ?, ?)", userRoleID, user.ID, roleID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to assign admin role to user: %v", err)
|
||||
}
|
||||
|
||||
user.Roles = []string{"admin"}
|
||||
return user
|
||||
}
|
||||
|
||||
func insertTestDatabaseCredentials(t *testing.T, db *sql.DB) {
|
||||
query := `INSERT INTO database (id, host, port, name, user, password)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
_, err := db.Exec(query, "db123", "localhost", 5432, "testdb", "postgres", "securepassword")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert test database credentials: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseCredentials(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestAdminUser(t, db)
|
||||
insertTestDatabaseCredentials(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
|
||||
token := "testtoken123456"
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
tokenID := "token123"
|
||||
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
|
||||
_, err := db.Exec(query, tokenID, user.ID, token, expires)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert test token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/v1/database/credentials?username="+user.User, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handleDatabaseCredentials(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
var response DatabaseCredentials
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Host != "localhost" {
|
||||
t.Errorf("Expected host 'localhost', got '%s'", response.Host)
|
||||
}
|
||||
if response.Port != 5432 {
|
||||
t.Errorf("Expected port 5432, got %d", response.Port)
|
||||
}
|
||||
if response.Name != "testdb" {
|
||||
t.Errorf("Expected database name 'testdb', got '%s'", response.Name)
|
||||
}
|
||||
if response.User != "postgres" {
|
||||
t.Errorf("Expected user 'postgres', got '%s'", response.User)
|
||||
}
|
||||
if response.Password != "securepassword" {
|
||||
t.Errorf("Expected password 'securepassword', got '%s'", response.Password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseCredentialsUnauthorized(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
user := createTestUser(t, db) // Regular user without admin role
|
||||
insertTestDatabaseCredentials(t, db)
|
||||
|
||||
logger := log.New(os.Stdout, "TEST: ", log.LstdFlags)
|
||||
server := NewServer(db, logger)
|
||||
|
||||
token := "testtoken123456"
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
tokenID := "token123"
|
||||
query := `INSERT INTO tokens (id, uid, token, expires) VALUES (?, ?, ?, ?)`
|
||||
_, err := db.Exec(query, tokenID, user.ID, token, expires)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert test token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/v1/database/credentials?username="+user.User, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
server.handleDatabaseCredentials(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusForbidden {
|
||||
t.Errorf("Expected status code %d, got %d", http.StatusForbidden, recorder.Code)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
DB *sql.DB
|
||||
Router *http.ServeMux
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB, logger *log.Logger) *Server {
|
||||
s := &Server{
|
||||
DB: db,
|
||||
Router: http.NewServeMux(),
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
s.registerRoutes()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) registerRoutes() {
|
||||
s.Router.HandleFunc("POST /v1/login/password", s.handlePasswordLogin)
|
||||
s.Router.HandleFunc("POST /v1/login/token", s.handleTokenLogin)
|
||||
s.Router.HandleFunc("GET /v1/database/credentials", s.handleDatabaseCredentials)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.Router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
s.Logger.Printf("Starting server on %s", addr)
|
||||
return http.ListenAndServe(addr, s)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ func (u *User) Check(login *Login) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(derived, u.Password) != 0 {
|
||||
if subtle.ConstantTimeCompare(derived, u.Password) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserRegisterAndCheck(t *testing.T) {
|
||||
user := &User{}
|
||||
login := &Login{
|
||||
User: "testuser",
|
||||
Password: "testpassword",
|
||||
}
|
||||
|
||||
err := user.Register(login)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to register user: %v", err)
|
||||
}
|
||||
|
||||
if user.ID == "" {
|
||||
t.Error("Expected user ID to be set, got empty string")
|
||||
}
|
||||
if user.User != "testuser" {
|
||||
t.Errorf("Expected username 'testuser', got '%s'", user.User)
|
||||
}
|
||||
if len(user.Password) == 0 {
|
||||
t.Error("Expected password to be hashed and set, got empty slice")
|
||||
}
|
||||
if len(user.Salt) == 0 {
|
||||
t.Error("Expected salt to be set, got empty slice")
|
||||
}
|
||||
|
||||
correctLogin := &Login{
|
||||
User: "testuser",
|
||||
Password: "testpassword",
|
||||
}
|
||||
if !user.Check(correctLogin) {
|
||||
t.Error("Check failed for correct password")
|
||||
}
|
||||
|
||||
incorrectLogin := &Login{
|
||||
User: "testuser",
|
||||
Password: "wrongpassword",
|
||||
}
|
||||
if user.Check(incorrectLogin) {
|
||||
t.Error("Check passed for incorrect password")
|
||||
}
|
||||
|
||||
wrongUserLogin := &Login{
|
||||
User: "wronguser",
|
||||
Password: "testpassword",
|
||||
}
|
||||
if user.Check(wrongUserLogin) {
|
||||
t.Error("Check passed for incorrect username")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSalt(t *testing.T) {
|
||||
salt, err := Salt()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate salt: %v", err)
|
||||
}
|
||||
|
||||
if len(salt) != saltLength {
|
||||
t.Errorf("Expected salt length %d, got %d", saltLength, len(salt))
|
||||
}
|
||||
|
||||
salt2, err := Salt()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate second salt: %v", err)
|
||||
}
|
||||
|
||||
// Check that salts are different (extremely unlikely to be the same)
|
||||
different := false
|
||||
for i := 0; i < len(salt); i++ {
|
||||
if salt[i] != salt2[i] {
|
||||
different = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !different {
|
||||
t.Error("Expected different salts, got identical values")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Package database provides database-related functionality for the MCIAS system.
|
||||
package database
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schemaFS embed.FS
|
||||
|
||||
// DefaultSchema returns the default database schema as a string.
|
||||
func DefaultSchema() (string, error) {
|
||||
schemaBytes, err := schemaFS.ReadFile("schema.sql")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(schemaBytes), nil
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
CREATE TABLE users (
|
||||
id text primary key,
|
||||
created integer,
|
||||
user text not null,
|
||||
password blob not null,
|
||||
salt blob not null
|
||||
);
|
||||
|
||||
CREATE TABLE tokens (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
token text not null,
|
||||
expires integer default 0,
|
||||
FOREIGN KEY(uid) REFERENCES user(id)
|
||||
);
|
||||
|
||||
CREATE TABLE database (
|
||||
id text primary key,
|
||||
host text not null,
|
||||
port integer default 5432,
|
||||
name text not null,
|
||||
user text not null,
|
||||
password text not null
|
||||
);
|
||||
|
||||
CREATE TABLE registrations (
|
||||
id text primary key,
|
||||
code text not null
|
||||
);
|
||||
|
||||
CREATE TABLE roles (
|
||||
id text primary key,
|
||||
role text not null
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
rid text not null,
|
||||
FOREIGN KEY(uid) REFERENCES user(id),
|
||||
FOREIGN KEY(rid) REFERENCES roles(id)
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
# MCIAS Documentation
|
||||
|
||||
Welcome to the MCIAS (Metacircular Identity and Access System) documentation. This directory contains comprehensive documentation for understanding, installing, and using the MCIAS system.
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### [Overview](overview.md)
|
||||
- Project overview
|
||||
- System architecture
|
||||
- Database schema
|
||||
- Security considerations
|
||||
|
||||
### [API Documentation](api.md)
|
||||
- API endpoints
|
||||
- Request/response formats
|
||||
- Error handling
|
||||
- Authentication flow
|
||||
|
||||
### [Installation and Usage Guide](installation.md)
|
||||
- Prerequisites
|
||||
- Installation steps
|
||||
- Running the server
|
||||
- Configuration options
|
||||
- Building from source
|
||||
- Development practices
|
||||
- API usage examples
|
||||
- Troubleshooting
|
||||
- Security best practices
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Project Repository](https://git.wntrmute.dev/kyle/mcias)
|
||||
- [Schema File](../schema.sql) - Database schema definition
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
When contributing to this documentation, please follow these guidelines:
|
||||
|
||||
1. Use Markdown format for all documentation files
|
||||
2. Keep the documentation up-to-date with code changes
|
||||
3. Include examples where appropriate
|
||||
4. Organize content logically with clear headings
|
||||
5. Use code blocks with syntax highlighting for code examples
|
|
@ -0,0 +1,46 @@
|
|||
#+title: MCIAS Documentation
|
||||
#+created: <2025-05-09 Fri 13:42>
|
||||
|
||||
* MCIAS Documentation
|
||||
|
||||
Welcome to the MCIAS (Metacircular Identity and Access System) documentation. This directory contains comprehensive documentation for understanding, installing, and using the MCIAS system.
|
||||
|
||||
* Documentation Index
|
||||
|
||||
** [[file:overview.org][Overview]]
|
||||
- Project overview
|
||||
- System architecture
|
||||
- Database schema
|
||||
- Security considerations
|
||||
|
||||
** [[file:api.org][API Documentation]]
|
||||
- API endpoints
|
||||
- Request/response formats
|
||||
- Error handling
|
||||
- Authentication flow
|
||||
|
||||
** [[file:installation.org][Installation and Usage Guide]]
|
||||
- Prerequisites
|
||||
- Installation steps
|
||||
- Running the server
|
||||
- Configuration options
|
||||
- Building from source
|
||||
- Development practices
|
||||
- API usage examples
|
||||
- Troubleshooting
|
||||
- Security best practices
|
||||
|
||||
* Additional Resources
|
||||
|
||||
- [[https://git.wntrmute.dev/kyle/mcias][Project Repository]]
|
||||
- [[file:../schema.sql][Schema File]] - Database schema definition
|
||||
|
||||
* Contributing to Documentation
|
||||
|
||||
When contributing to this documentation, please follow these guidelines:
|
||||
|
||||
1. Use org-mode format for all documentation files
|
||||
2. Keep the documentation up-to-date with code changes
|
||||
3. Include examples where appropriate
|
||||
4. Organize content logically with clear headings
|
||||
5. Use code blocks with syntax highlighting for code examples
|
|
@ -0,0 +1,125 @@
|
|||
# MCIAS API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
MCIAS (Metacircular Identity and Access System) provides identity and authentication services across metacircular projects. This document describes the REST API endpoints, request/response formats, and error handling.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
|
||||
#### Password-based Authentication
|
||||
|
||||
**Endpoint**: `POST /v1/login/password`
|
||||
|
||||
**Description**: Authenticates a user using username and password credentials.
|
||||
|
||||
**Request Format**:
|
||||
```json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "secret_password"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields**:
|
||||
- `version`: Must be "v1"
|
||||
- `login.user`: Username
|
||||
- `login.password`: User's password
|
||||
|
||||
**Response Format** (Success - 200 OK):
|
||||
```json
|
||||
{
|
||||
"token": "authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields**:
|
||||
- `token`: Authentication token to be used for subsequent requests
|
||||
- `expires`: Unix timestamp when the token expires
|
||||
|
||||
**Error Responses**:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
- 401 Unauthorized: Invalid username or password
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
#### Token-based Authentication
|
||||
|
||||
**Endpoint**: `POST /v1/login/token`
|
||||
|
||||
**Description**: Authenticates a user using a previously issued token.
|
||||
|
||||
**Request Format**:
|
||||
```json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"token": "existing_token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields**:
|
||||
- `version`: Must be "v1"
|
||||
- `login.user`: Username
|
||||
- `login.token`: Previously issued authentication token
|
||||
|
||||
**Response Format** (Success - 200 OK):
|
||||
```json
|
||||
{
|
||||
"token": "new_authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields**:
|
||||
- `token`: New authentication token to be used for subsequent requests
|
||||
- `expires`: Unix timestamp when the token expires
|
||||
|
||||
**Error Responses**:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
- 401 Unauthorized: Invalid or expired token
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
### Database Credentials
|
||||
|
||||
**Endpoint**: `/v1/credentials/database` (Not yet implemented)
|
||||
|
||||
**Description**: Retrieves database credentials for authorized users.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All error responses follow a standard format:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Error message describing the issue"
|
||||
}
|
||||
```
|
||||
|
||||
Common HTTP status codes:
|
||||
- 200 OK: Request successful
|
||||
- 400 Bad Request: Invalid request format or parameters
|
||||
- 401 Unauthorized: Authentication failed
|
||||
- 403 Forbidden: Insufficient permissions
|
||||
- 404 Not Found: Resource not found
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. **Initial Authentication**:
|
||||
- Client sends username and password to `/v1/login/password`
|
||||
- Server validates credentials and returns a token
|
||||
|
||||
2. **Subsequent Requests**:
|
||||
- Client uses the token for authentication by sending it to `/v1/login/token`
|
||||
- Server validates the token and issues a new token
|
||||
|
||||
3. **Token Expiration**:
|
||||
- Tokens expire after 24 hours
|
||||
- Clients should request a new token before expiration
|
|
@ -0,0 +1,128 @@
|
|||
#+title: MCIAS API Documentation
|
||||
#+created: <2025-05-09 Fri 13:42>
|
||||
|
||||
* MCIAS API Documentation
|
||||
|
||||
** Overview
|
||||
|
||||
MCIAS (Metacircular Identity and Access System) provides identity and authentication services across metacircular projects. This document describes the REST API endpoints, request/response formats, and error handling.
|
||||
|
||||
** API Endpoints
|
||||
|
||||
*** Authentication
|
||||
|
||||
**** Password-based Authentication
|
||||
|
||||
*Endpoint*: =POST /v1/login/password=
|
||||
|
||||
*Description*: Authenticates a user using username and password credentials.
|
||||
|
||||
*Request Format*:
|
||||
#+begin_src json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "secret_password"
|
||||
}
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Required Fields*:
|
||||
- =version=: Must be "v1"
|
||||
- =login.user=: Username
|
||||
- =login.password=: User's password
|
||||
|
||||
*Response Format* (Success - 200 OK):
|
||||
#+begin_src json
|
||||
{
|
||||
"token": "authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Response Fields*:
|
||||
- =token=: Authentication token to be used for subsequent requests
|
||||
- =expires=: Unix timestamp when the token expires
|
||||
|
||||
*Error Responses*:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
- 401 Unauthorized: Invalid username or password
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
**** Token-based Authentication
|
||||
|
||||
*Endpoint*: =POST /v1/login/token=
|
||||
|
||||
*Description*: Authenticates a user using a previously issued token.
|
||||
|
||||
*Request Format*:
|
||||
#+begin_src json
|
||||
{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"token": "existing_token"
|
||||
}
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Required Fields*:
|
||||
- =version=: Must be "v1"
|
||||
- =login.user=: Username
|
||||
- =login.token=: Previously issued authentication token
|
||||
|
||||
*Response Format* (Success - 200 OK):
|
||||
#+begin_src json
|
||||
{
|
||||
"token": "new_authentication_token",
|
||||
"expires": 1621234567
|
||||
}
|
||||
#+end_src
|
||||
|
||||
*Response Fields*:
|
||||
- =token=: New authentication token to be used for subsequent requests
|
||||
- =expires=: Unix timestamp when the token expires
|
||||
|
||||
*Error Responses*:
|
||||
- 400 Bad Request: Invalid request format or missing required fields
|
||||
- 401 Unauthorized: Invalid or expired token
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
*** Database Credentials
|
||||
|
||||
*Endpoint*: =/v1/credentials/database= (Not yet implemented)
|
||||
|
||||
*Description*: Retrieves database credentials for authorized users.
|
||||
|
||||
** Error Handling
|
||||
|
||||
All error responses follow a standard format:
|
||||
|
||||
#+begin_src json
|
||||
{
|
||||
"error": "Error message describing the issue"
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Common HTTP status codes:
|
||||
- 200 OK: Request successful
|
||||
- 400 Bad Request: Invalid request format or parameters
|
||||
- 401 Unauthorized: Authentication failed
|
||||
- 403 Forbidden: Insufficient permissions
|
||||
- 404 Not Found: Resource not found
|
||||
- 500 Internal Server Error: Server-side error
|
||||
|
||||
** Authentication Flow
|
||||
|
||||
1. *Initial Authentication*:
|
||||
- Client sends username and password to =/v1/login/password=
|
||||
- Server validates credentials and returns a token
|
||||
|
||||
2. *Subsequent Requests*:
|
||||
- Client uses the token for authentication by sending it to =/v1/login/token=
|
||||
- Server validates the token and issues a new token
|
||||
|
||||
3. *Token Expiration*:
|
||||
- Tokens expire after 24 hours
|
||||
- Clients should request a new token before expiration
|
|
@ -0,0 +1,170 @@
|
|||
# MCIAS Installation and Usage Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing MCIAS, ensure you have the following prerequisites:
|
||||
|
||||
- Go 1.23 or later
|
||||
- SQLite3
|
||||
- golangci-lint (for development)
|
||||
|
||||
## Installation
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone git.wntrmute.dev/kyle/mcias
|
||||
cd mcias
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
### Initialize the Database
|
||||
|
||||
MCIAS uses SQLite for data storage. To initialize the database:
|
||||
|
||||
```bash
|
||||
go run main.go -init -db ./mcias.db
|
||||
```
|
||||
|
||||
This command creates a new SQLite database file and initializes it with the schema defined in `schema.sql`.
|
||||
|
||||
## Running the Server
|
||||
|
||||
### Basic Usage
|
||||
|
||||
To start the MCIAS server with default settings:
|
||||
|
||||
```bash
|
||||
go run main.go -db ./mcias.db
|
||||
```
|
||||
|
||||
By default, the server listens on port 8080.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
MCIAS supports the following command-line options:
|
||||
|
||||
- `-db <path>`: Path to the SQLite database file (default: `mcias.db`)
|
||||
- `-addr <address>`: Address to listen on (default: `:8080`)
|
||||
- `-init`: Initialize the database and exit
|
||||
|
||||
Example with custom port:
|
||||
|
||||
```bash
|
||||
go run main.go -db ./mcias.db -addr :9000
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
To build a binary:
|
||||
|
||||
```bash
|
||||
go build -o mcias
|
||||
```
|
||||
|
||||
Then run the binary:
|
||||
|
||||
```bash
|
||||
./mcias -db ./mcias.db
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run all tests:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Linting
|
||||
|
||||
To run the linter:
|
||||
|
||||
```bash
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
## Using the API
|
||||
|
||||
### Authentication with Password
|
||||
|
||||
To authenticate a user with a password:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/login/password \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Authentication with Token
|
||||
|
||||
To authenticate a user with a token:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/login/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"token": "your_token"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Database errors**: Ensure the database file exists and has the correct permissions.
|
||||
```bash
|
||||
# Check permissions
|
||||
ls -l mcias.db
|
||||
# Fix permissions if needed
|
||||
chmod 644 mcias.db
|
||||
```
|
||||
|
||||
2. **Port already in use**: If the port is already in use, specify a different port with the `-addr` flag.
|
||||
```bash
|
||||
go run main.go -db ./mcias.db -addr :8081
|
||||
```
|
||||
|
||||
3. **Authentication failures**: Ensure you're using the correct username and password/token.
|
||||
|
||||
### Logging
|
||||
|
||||
MCIAS logs to stdout by default. To capture logs to a file:
|
||||
|
||||
```bash
|
||||
go run main.go -db ./mcias.db > mcias.log 2>&1
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Production Deployment**:
|
||||
- Use HTTPS in production
|
||||
- Set up proper firewall rules
|
||||
- Run the service with minimal privileges
|
||||
|
||||
2. **Database Security**:
|
||||
- Regularly backup the database
|
||||
- Restrict file permissions on the database file
|
||||
- Consider encrypting the database file at rest
|
||||
|
||||
3. **User Management**:
|
||||
- Implement strong password policies
|
||||
- Regularly rotate tokens
|
||||
- Monitor for suspicious authentication attempts
|
|
@ -0,0 +1,173 @@
|
|||
#+title: MCIAS Installation and Usage Guide
|
||||
#+created: <2025-05-09 Fri 13:42>
|
||||
|
||||
* MCIAS Installation and Usage Guide
|
||||
|
||||
** Prerequisites
|
||||
|
||||
Before installing MCIAS, ensure you have the following prerequisites:
|
||||
|
||||
- Go 1.23 or later
|
||||
- SQLite3
|
||||
- golangci-lint (for development)
|
||||
|
||||
** Installation
|
||||
|
||||
*** Clone the Repository
|
||||
|
||||
#+begin_src bash
|
||||
git clone git.wntrmute.dev/kyle/mcias
|
||||
cd mcias
|
||||
#+end_src
|
||||
|
||||
*** Install Dependencies
|
||||
|
||||
#+begin_src bash
|
||||
go mod download
|
||||
#+end_src
|
||||
|
||||
*** Initialize the Database
|
||||
|
||||
MCIAS uses SQLite for data storage. To initialize the database:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -init -db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
This command creates a new SQLite database file and initializes it with the schema defined in =schema.sql=.
|
||||
|
||||
** Running the Server
|
||||
|
||||
*** Basic Usage
|
||||
|
||||
To start the MCIAS server with default settings:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
By default, the server listens on port 8080.
|
||||
|
||||
*** Configuration Options
|
||||
|
||||
MCIAS supports the following command-line options:
|
||||
|
||||
- =-db <path>=: Path to the SQLite database file (default: =mcias.db=)
|
||||
- =-addr <address>=: Address to listen on (default: =:8080=)
|
||||
- =-init=: Initialize the database and exit
|
||||
|
||||
Example with custom port:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db -addr :9000
|
||||
#+end_src
|
||||
|
||||
** Building from Source
|
||||
|
||||
To build a binary:
|
||||
|
||||
#+begin_src bash
|
||||
go build -o mcias
|
||||
#+end_src
|
||||
|
||||
Then run the binary:
|
||||
|
||||
#+begin_src bash
|
||||
./mcias -db ./mcias.db
|
||||
#+end_src
|
||||
|
||||
** Development
|
||||
|
||||
*** Running Tests
|
||||
|
||||
To run all tests:
|
||||
|
||||
#+begin_src bash
|
||||
go test ./...
|
||||
#+end_src
|
||||
|
||||
*** Linting
|
||||
|
||||
To run the linter:
|
||||
|
||||
#+begin_src bash
|
||||
golangci-lint run
|
||||
#+end_src
|
||||
|
||||
** Using the API
|
||||
|
||||
*** Authentication with Password
|
||||
|
||||
To authenticate a user with a password:
|
||||
|
||||
#+begin_src bash
|
||||
curl -X POST http://localhost:8080/v1/login/password \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}'
|
||||
#+end_src
|
||||
|
||||
*** Authentication with Token
|
||||
|
||||
To authenticate a user with a token:
|
||||
|
||||
#+begin_src bash
|
||||
curl -X POST http://localhost:8080/v1/login/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "v1",
|
||||
"login": {
|
||||
"user": "username",
|
||||
"token": "your_token"
|
||||
}
|
||||
}'
|
||||
#+end_src
|
||||
|
||||
** Troubleshooting
|
||||
|
||||
*** Common Issues
|
||||
|
||||
1. *Database errors*: Ensure the database file exists and has the correct permissions.
|
||||
#+begin_src bash
|
||||
# Check permissions
|
||||
ls -l mcias.db
|
||||
# Fix permissions if needed
|
||||
chmod 644 mcias.db
|
||||
#+end_src
|
||||
|
||||
2. *Port already in use*: If the port is already in use, specify a different port with the =-addr= flag.
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db -addr :8081
|
||||
#+end_src
|
||||
|
||||
3. *Authentication failures*: Ensure you're using the correct username and password/token.
|
||||
|
||||
*** Logging
|
||||
|
||||
MCIAS logs to stdout by default. To capture logs to a file:
|
||||
|
||||
#+begin_src bash
|
||||
go run main.go -db ./mcias.db > mcias.log 2>&1
|
||||
#+end_src
|
||||
|
||||
** Security Best Practices
|
||||
|
||||
1. *Production Deployment*:
|
||||
- Use HTTPS in production
|
||||
- Set up proper firewall rules
|
||||
- Run the service with minimal privileges
|
||||
|
||||
2. *Database Security*:
|
||||
- Regularly backup the database
|
||||
- Restrict file permissions on the database file
|
||||
- Consider encrypting the database file at rest
|
||||
|
||||
3. *User Management*:
|
||||
- Implement strong password policies
|
||||
- Regularly rotate tokens
|
||||
- Monitor for suspicious authentication attempts
|
|
@ -0,0 +1,130 @@
|
|||
# MCIAS: Metacircular Identity and Access System
|
||||
|
||||
## Project Overview
|
||||
|
||||
MCIAS (Metacircular Identity and Access System) is a centralized identity and access management system designed to provide authentication and authorization services across metacircular projects. It serves as a single source of truth for user identity and access control.
|
||||
|
||||
The system currently provides:
|
||||
1. User password authentication
|
||||
2. User token authentication
|
||||
3. Database credential authentication
|
||||
|
||||
Future planned features include:
|
||||
1. TOTP (Time-based One-Time Password) authentication
|
||||
2. Policy management for fine-grained access control
|
||||
|
||||
## System Architecture
|
||||
|
||||
MCIAS is built as a standalone REST API service with the following components:
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **API Layer** (`api/` directory)
|
||||
- HTTP server and routing
|
||||
- Request/response handling
|
||||
- Authentication endpoints
|
||||
- Error handling
|
||||
|
||||
2. **Data Layer** (`data/` directory)
|
||||
- User management
|
||||
- Token management
|
||||
- Password hashing and verification
|
||||
- Secure random generation
|
||||
|
||||
3. **Database** (SQLite)
|
||||
- Persistent storage for users, tokens, and credentials
|
||||
- Schema defined in `schema.sql`
|
||||
|
||||
### Request Flow
|
||||
|
||||
1. Client sends authentication request to the API
|
||||
2. API layer validates the request format
|
||||
3. Data layer processes the authentication logic
|
||||
4. Database is queried to verify credentials
|
||||
5. Response is generated and sent back to the client
|
||||
|
||||
## Database Schema
|
||||
|
||||
MCIAS uses a SQLite database with the following tables:
|
||||
|
||||
### Users Table
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id text primary key,
|
||||
created integer,
|
||||
user text not null,
|
||||
password blob not null,
|
||||
salt blob not null
|
||||
);
|
||||
```
|
||||
|
||||
### Tokens Table
|
||||
```sql
|
||||
CREATE TABLE tokens (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
token text not null,
|
||||
expires integer default 0,
|
||||
FOREIGN KEY(uid) REFERENCES user(id)
|
||||
);
|
||||
```
|
||||
|
||||
### Database Credentials Table
|
||||
```sql
|
||||
CREATE TABLE database (
|
||||
id text primary key,
|
||||
host text not null,
|
||||
port integer default 5432,
|
||||
name text not null,
|
||||
user text not null,
|
||||
password text not null
|
||||
);
|
||||
```
|
||||
|
||||
### Registrations Table
|
||||
```sql
|
||||
CREATE TABLE registrations (
|
||||
id text primary key,
|
||||
code text not null
|
||||
);
|
||||
```
|
||||
|
||||
### Roles Tables
|
||||
```sql
|
||||
CREATE TABLE roles (
|
||||
id text primary key,
|
||||
role text not null
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
rid text not null,
|
||||
FOREIGN KEY(uid) REFERENCES user(id),
|
||||
FOREIGN KEY(rid) REFERENCES roles(id)
|
||||
);
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
MCIAS implements several security best practices:
|
||||
|
||||
1. **Password Security**
|
||||
- Passwords are never stored in plaintext
|
||||
- Scrypt key derivation function is used for password hashing
|
||||
- Each user has a unique random salt
|
||||
- Constant-time comparison is used to prevent timing attacks
|
||||
|
||||
2. **Token Security**
|
||||
- Tokens are generated using cryptographically secure random functions
|
||||
- Tokens have an expiration time (24 hours by default)
|
||||
- New tokens are issued on each successful authentication
|
||||
|
||||
3. **API Security**
|
||||
- Input validation on all endpoints
|
||||
- Standardized error responses that don't leak sensitive information
|
||||
- Rate limiting (to be implemented)
|
||||
|
||||
4. **Database Security**
|
||||
- Parameterized queries to prevent SQL injection
|
||||
- Foreign key constraints to maintain data integrity
|
|
@ -0,0 +1,133 @@
|
|||
#+title: MCIAS: Metacircular Identity and Access System
|
||||
#+created: <2025-05-09 Fri 13:42>
|
||||
|
||||
* MCIAS: Metacircular Identity and Access System
|
||||
|
||||
** Project Overview
|
||||
|
||||
MCIAS (Metacircular Identity and Access System) is a centralized identity and access management system designed to provide authentication and authorization services across metacircular projects. It serves as a single source of truth for user identity and access control.
|
||||
|
||||
The system currently provides:
|
||||
1. User password authentication
|
||||
2. User token authentication
|
||||
3. Database credential authentication
|
||||
|
||||
Future planned features include:
|
||||
1. TOTP (Time-based One-Time Password) authentication
|
||||
2. Policy management for fine-grained access control
|
||||
|
||||
** System Architecture
|
||||
|
||||
MCIAS is built as a standalone REST API service with the following components:
|
||||
|
||||
*** Core Components
|
||||
|
||||
1. *API Layer* (=api/= directory)
|
||||
- HTTP server and routing
|
||||
- Request/response handling
|
||||
- Authentication endpoints
|
||||
- Error handling
|
||||
|
||||
2. *Data Layer* (=data/= directory)
|
||||
- User management
|
||||
- Token management
|
||||
- Password hashing and verification
|
||||
- Secure random generation
|
||||
|
||||
3. *Database* (SQLite)
|
||||
- Persistent storage for users, tokens, and credentials
|
||||
- Schema defined in =schema.sql=
|
||||
|
||||
*** Request Flow
|
||||
|
||||
1. Client sends authentication request to the API
|
||||
2. API layer validates the request format
|
||||
3. Data layer processes the authentication logic
|
||||
4. Database is queried to verify credentials
|
||||
5. Response is generated and sent back to the client
|
||||
|
||||
** Database Schema
|
||||
|
||||
MCIAS uses a SQLite database with the following tables:
|
||||
|
||||
*** Users Table
|
||||
#+begin_src sql
|
||||
CREATE TABLE users (
|
||||
id text primary key,
|
||||
created integer,
|
||||
user text not null,
|
||||
password blob not null,
|
||||
salt blob not null
|
||||
);
|
||||
#+end_src
|
||||
|
||||
*** Tokens Table
|
||||
#+begin_src sql
|
||||
CREATE TABLE tokens (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
token text not null,
|
||||
expires integer default 0,
|
||||
FOREIGN KEY(uid) REFERENCES user(id)
|
||||
);
|
||||
#+end_src
|
||||
|
||||
*** Database Credentials Table
|
||||
#+begin_src sql
|
||||
CREATE TABLE database (
|
||||
id text primary key,
|
||||
host text not null,
|
||||
port integer default 5432,
|
||||
name text not null,
|
||||
user text not null,
|
||||
password text not null
|
||||
);
|
||||
#+end_src
|
||||
|
||||
*** Registrations Table
|
||||
#+begin_src sql
|
||||
CREATE TABLE registrations (
|
||||
id text primary key,
|
||||
code text not null
|
||||
);
|
||||
#+end_src
|
||||
|
||||
*** Roles Tables
|
||||
#+begin_src sql
|
||||
CREATE TABLE roles (
|
||||
id text primary key,
|
||||
role text not null
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
id text primary key,
|
||||
uid text not null,
|
||||
rid text not null,
|
||||
FOREIGN KEY(uid) REFERENCES user(id),
|
||||
FOREIGN KEY(rid) REFERENCES roles(id)
|
||||
);
|
||||
#+end_src
|
||||
|
||||
** Security Considerations
|
||||
|
||||
MCIAS implements several security best practices:
|
||||
|
||||
1. *Password Security*
|
||||
- Passwords are never stored in plaintext
|
||||
- Scrypt key derivation function is used for password hashing
|
||||
- Each user has a unique random salt
|
||||
- Constant-time comparison is used to prevent timing attacks
|
||||
|
||||
2. *Token Security*
|
||||
- Tokens are generated using cryptographically secure random functions
|
||||
- Tokens have an expiration time (24 hours by default)
|
||||
- New tokens are issued on each successful authentication
|
||||
|
||||
3. *API Security*
|
||||
- Input validation on all endpoints
|
||||
- Standardized error responses that don't leak sensitive information
|
||||
- Rate limiting (to be implemented)
|
||||
|
||||
4. *Database Security*
|
||||
- Parameterized queries to prevent SQL injection
|
||||
- Foreign key constraints to maintain data integrity
|
17
go.mod
17
go.mod
|
@ -3,7 +3,24 @@ module git.wntrmute.dev/kyle/mcias
|
|||
go 1.23.8
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
42
go.sum
42
go.sum
|
@ -1,7 +1,49 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd := exec.Command("go", "run", "cmd/mcias/main.go")
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running mcias command: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue