diff --git a/.coderabbit.yaml b/.coderabbit.yaml index de87f17..e3b9d3a 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,17 +1,16 @@ # CodeRabbit Configuration File # Reference: https://docs.coderabbit.ai/spec/configuration +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json language: "en-US" -tone_instruction: "Review code as a senior Go software engineer focusing on security practices, cryptographic hashing correctness, token lifecycles, and RBAC authorization." reviews: profile: "assertive" + tone_instructions: "Review code as a senior Go software engineer focusing on security practices, cryptographic hashing correctness, token lifecycles, and RBAC authorization." auto_review: enabled: true drafts: false high_level_summary: true - reviews_per_file: true - collapse_dependency_reviews: true chat: auto_reply: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e5d8f3..6192b0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,16 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - name: Checkout auth + uses: actions/checkout@v4 + - name: Checkout mdk + uses: actions/checkout@v4 + with: + repository: GoHyperrr/mdk + ref: ${{ github.head_ref || github.ref_name }} + path: mdk + - name: Move mdk to parent directory + run: mv mdk ../mdk - uses: actions/setup-go@v5 with: go-version: '1.25' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6647cb2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - 2026-06-05 + +### Added +- Concrete auth.Actor persistence model implementing mdk.Actor interface. +- Dynamic apikey and emailpass authentication providers. +- Decoupled command line runner integrations. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..662d04c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +For pre-1.0 releases we support the latest release or the latest patch series (e.g., 0.1.x), and for 1.0+ we support the latest major release branch. + +## Reporting a Vulnerability + +If you discover a security vulnerability within Hyperrr or any of its modules, please do not disclose it publicly. Report it directly by emailing security@hyperrr.org or opening a draft security advisory on GitHub. + +We will acknowledge receipt of your report within 48 hours and work with you to patch the issue promptly. diff --git a/actor.go b/actor.go new file mode 100644 index 0000000..01d9de2 --- /dev/null +++ b/actor.go @@ -0,0 +1,37 @@ +package auth + +import ( + "time" + + "github.com/GoHyperrr/mdk" +) + +// Actor is the concrete GORM database model representing a security principal. +// It implements the mdk.Actor interface. +type Actor struct { + ID string `gorm:"primaryKey" json:"id"` + Type mdk.ActorType `gorm:"index" json:"type"` + Name string `json:"name"` + Metadata mdk.JSONMap `gorm:"type:text" json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Ensure Actor implements mdk.Actor. +var _ mdk.Actor = (*Actor)(nil) + +func (a *Actor) GetID() string { + return a.ID +} + +func (a *Actor) GetType() mdk.ActorType { + return a.Type +} + +func (a *Actor) GetName() string { + return a.Name +} + +func (a *Actor) GetMetadata() map[string]string { + return a.Metadata +} diff --git a/apikey/cli.go b/apikey/cli.go index 615c3e2..1ea1476 100644 --- a/apikey/cli.go +++ b/apikey/cli.go @@ -6,12 +6,13 @@ import ( "fmt" "time" + "github.com/GoHyperrr/auth" "github.com/GoHyperrr/mdk" "github.com/google/uuid" ) -// runAPIKeyCmd executes the CLI logic to generate a new API key. -func runAPIKeyCmd(rt mdk.Runtime, args []string) error { +// RunAPIKeyCmd executes the CLI logic to generate a new API key. +func RunAPIKeyCmd(rt mdk.Runtime, args []string) error { if len(args) < 1 || args[0] != "generate" { fmt.Println("Usage: hyperrr apikey generate") return fmt.Errorf("invalid arguments") @@ -23,16 +24,16 @@ func runAPIKeyCmd(rt mdk.Runtime, args []string) error { } // Auto-migrate tables locally to make sure Actors and APIKeys exist - err := database.AutoMigrate(&mdk.Actor{}, &APIKey{}) + err := database.AutoMigrate(&auth.Actor{}, &APIKey{}) if err != nil { return fmt.Errorf("failed to run migrations for apikey models: %w", err) } // Seed default MCP Developer Actor if not already present var actorCount int64 - database.Model(&mdk.Actor{}).Where("id = ?", "act_mcp_developer").Count(&actorCount) + database.Model(&auth.Actor{}).Where("id = ?", "act_mcp_developer").Count(&actorCount) if actorCount == 0 { - devActor := mdk.Actor{ + devActor := auth.Actor{ ID: "act_mcp_developer", Type: mdk.ActorAIAgent, Name: "Developer Agent", diff --git a/apikey/graphql.go b/apikey/graphql.go index da52438..570c55f 100644 --- a/apikey/graphql.go +++ b/apikey/graphql.go @@ -31,7 +31,7 @@ func (m *Module) CreateAPIKeyResolver(ctx context.Context, name string, expiresA return nil, fmt.Errorf("unauthorized") } - key, err := m.CreateAPIKey(ctx, actor.ID, name, expiresAt) + key, err := m.CreateAPIKey(ctx, actor.GetID(), name, expiresAt) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func (m *Module) RevokeAPIKeyResolver(ctx context.Context, id string) (bool, err return false, fmt.Errorf("unauthorized") } - return m.RevokeAPIKey(ctx, actor.ID, id) + return m.RevokeAPIKey(ctx, actor.GetID(), id) } func (m *Module) ListAPIKeysResolver(ctx context.Context) ([]*APIKeyInfo, error) { @@ -61,7 +61,7 @@ func (m *Module) ListAPIKeysResolver(ctx context.Context) ([]*APIKeyInfo, error) return nil, fmt.Errorf("unauthorized") } - keys, err := m.ListAPIKeys(ctx, actor.ID) + keys, err := m.ListAPIKeys(ctx, actor.GetID()) if err != nil { return nil, err } diff --git a/apikey/handlers.go b/apikey/handlers.go index 42e7898..a3d683e 100644 --- a/apikey/handlers.go +++ b/apikey/handlers.go @@ -12,7 +12,7 @@ import ( ) // GetActorByAPIKey retrieves an actor associated with a given API key. -func (m *Module) GetActorByAPIKey(ctx context.Context, key string) (*mdk.Actor, error) { +func (m *Module) GetActorByAPIKey(ctx context.Context, key string) (mdk.Actor, error) { var apiKey APIKey err := m.database.WithContext(ctx).Preload("Actor").First(&apiKey, "key = ?", key).Error if err != nil { diff --git a/apikey/model.go b/apikey/model.go index 322491e..f2f875b 100644 --- a/apikey/model.go +++ b/apikey/model.go @@ -3,7 +3,7 @@ package apikey import ( "time" - "github.com/GoHyperrr/mdk" + "github.com/GoHyperrr/auth" "gorm.io/gorm" ) @@ -13,7 +13,7 @@ type APIKey struct { Name string `gorm:"default:'';not null" json:"name"` Key string `gorm:"uniqueIndex;not null" json:"key"` ActorID string `gorm:"not null" json:"actor_id"` - Actor mdk.Actor `gorm:"foreignKey:ActorID" json:"actor"` + Actor auth.Actor `gorm:"foreignKey:ActorID" json:"actor"` ExpiresAt *time.Time `json:"expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` diff --git a/apikey/module.go b/apikey/module.go index 8779155..1b87f3b 100644 --- a/apikey/module.go +++ b/apikey/module.go @@ -42,15 +42,5 @@ func init() { mdk.Register(func() mdk.Module { return NewModule() }) - - mdk.RegisterCommand(mdk.CLICommand{ - Group: "auth", - Name: "apikey", - Usage: "generate", - Short: "Generate a new secure API key on-demand", - Long: "Generate a new secure API key on-demand and write it to the database.", - NeedsDB: true, - Run: runAPIKeyCmd, - }) } diff --git a/emailpass/cli.go b/emailpass/cli.go index cac7bb2..473f67b 100644 --- a/emailpass/cli.go +++ b/emailpass/cli.go @@ -4,14 +4,15 @@ import ( "fmt" "strings" + "github.com/GoHyperrr/auth" "github.com/GoHyperrr/mdk" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) -// runEmailPassCmd registers a new user via CLI. -func runEmailPassCmd(rt mdk.Runtime, args []string) error { +// RunEmailPassCmd registers a new user via CLI. +func RunEmailPassCmd(rt mdk.Runtime, args []string) error { if len(args) < 4 || args[0] != "register" { fmt.Println("Usage: hyperrr emailpass register ") return fmt.Errorf("invalid arguments") @@ -27,7 +28,7 @@ func runEmailPassCmd(rt mdk.Runtime, args []string) error { } // Auto-migrate tables locally to ensure Actor and User exist - err := database.AutoMigrate(&mdk.Actor{}, &User{}) + err := database.AutoMigrate(&auth.Actor{}, &User{}) if err != nil { return fmt.Errorf("failed to run migrations for emailpass models: %w", err) } @@ -45,7 +46,7 @@ func runEmailPassCmd(rt mdk.Runtime, args []string) error { } actorID := "act_" + uuid.New().String() - actor := mdk.Actor{ + actor := auth.Actor{ ID: actorID, Type: mdk.ActorHuman, Name: name, diff --git a/emailpass/graphql.go b/emailpass/graphql.go index 0940559..7385a86 100644 --- a/emailpass/graphql.go +++ b/emailpass/graphql.go @@ -30,7 +30,7 @@ func (m *Module) RegisterResolver(ctx context.Context, email string, password st return nil, err } - token, err := m.store.GenerateToken(*actor) + token, err := m.store.GenerateToken(actor) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } @@ -47,7 +47,7 @@ func (m *Module) LoginResolver(ctx context.Context, email string, password strin return nil, err } - token, err := m.store.GenerateToken(*actor) + token, err := m.store.GenerateToken(actor) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } @@ -58,7 +58,7 @@ func (m *Module) LoginResolver(ctx context.Context, email string, password strin }, nil } -func (m *Module) Me(ctx context.Context) (*mdk.Actor, error) { +func (m *Module) Me(ctx context.Context) (mdk.Actor, error) { actor, ok := mdk.ActorFromContext(ctx) if !ok { return nil, fmt.Errorf("unauthorized") diff --git a/emailpass/handlers.go b/emailpass/handlers.go index 57284f2..e1a6262 100644 --- a/emailpass/handlers.go +++ b/emailpass/handlers.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/GoHyperrr/auth" "github.com/GoHyperrr/mdk" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" @@ -27,20 +28,20 @@ func (m *Module) ValidateActor(ctx context.Context, input any) (any, error) { return nil, fmt.Errorf("actor_id is required") } - var actor mdk.Actor + var actor auth.Actor if err := m.database.First(&actor, "id = ?", actorID).Error; err != nil { return nil, fmt.Errorf("actor not found: %w", err) } return map[string]any{ - "id": actor.ID, - "type": actor.Type, - "name": actor.Name, + "id": actor.GetID(), + "type": actor.GetType(), + "name": actor.GetName(), }, nil } // Register creates a new user and actor. -func (m *Module) Register(ctx context.Context, email, password, name string) (*mdk.Actor, error) { +func (m *Module) Register(ctx context.Context, email, password, name string) (mdk.Actor, error) { if email == "" || password == "" || name == "" { return nil, fmt.Errorf("email, password, and name are required") } @@ -50,7 +51,7 @@ func (m *Module) Register(ctx context.Context, email, password, name string) (*m } actorID := "act_" + uuid.New().String() - actor := mdk.Actor{ + actor := auth.Actor{ ID: actorID, Type: mdk.ActorHuman, Name: name, @@ -89,7 +90,7 @@ func (m *Module) Register(ctx context.Context, email, password, name string) (*m } // Login verifies credentials and returns the actor. -func (m *Module) Login(ctx context.Context, email, password string) (*mdk.Actor, error) { +func (m *Module) Login(ctx context.Context, email, password string) (mdk.Actor, error) { var user User if err := m.database.Preload("Actor").First(&user, "email = ?", email).Error; err != nil { return nil, fmt.Errorf("invalid credentials") diff --git a/emailpass/model.go b/emailpass/model.go index a5f7b94..a22f545 100644 --- a/emailpass/model.go +++ b/emailpass/model.go @@ -3,6 +3,7 @@ package emailpass import ( "time" + "github.com/GoHyperrr/auth" "github.com/GoHyperrr/mdk" "gorm.io/gorm" ) @@ -13,15 +14,15 @@ type User struct { Email string `gorm:"uniqueIndex;not null" json:"email"` PasswordHash string `gorm:"not null" json:"-"` ActorID string `gorm:"not null" json:"actor_id"` - Actor mdk.Actor `gorm:"foreignKey:ActorID" json:"actor"` + Actor auth.Actor `gorm:"foreignKey:ActorID" json:"actor"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` } type AuthResponse struct { - Token string `json:"token"` - Actor *mdk.Actor `json:"actor"` + Token string `json:"token"` + Actor mdk.Actor `json:"actor"` } diff --git a/emailpass/module.go b/emailpass/module.go index 6f88189..626cedf 100644 --- a/emailpass/module.go +++ b/emailpass/module.go @@ -94,7 +94,7 @@ func (m *Module) Routes() []mdk.Route { } // ValidateToken implements mdk.TokenValidator interface. -func (m *Module) ValidateToken(ctx context.Context, token string) (*mdk.Actor, error) { +func (m *Module) ValidateToken(ctx context.Context, token string) (mdk.Actor, error) { return m.store.ValidateToken(ctx, token) } @@ -144,15 +144,5 @@ func init() { mdk.Register(func() mdk.Module { return NewModule("", "") }) - - mdk.RegisterCommand(mdk.CLICommand{ - Group: "auth", - Name: "user", - Usage: "register ", - Short: "Register a new user via email/password", - Long: "Register a new user dynamically via email/password and write it to the database.", - NeedsDB: true, - Run: runEmailPassCmd, - }) } diff --git a/go.mod b/go.mod index 6f34307..b026aae 100644 --- a/go.mod +++ b/go.mod @@ -15,3 +15,5 @@ require ( github.com/jinzhu/now v1.1.5 // indirect golang.org/x/text v0.37.0 // indirect ) + +replace github.com/GoHyperrr/mdk => ../mdk diff --git a/go.sum b/go.sum index 176f027..81d78d9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/GoHyperrr/mdk v0.0.0-20260605044506-3d2ab0d97ca9 h1:32yHu/Gnk+ztrDUZ8D+WnjWmkKZd8ebs+yxrjKJFq8Y= -github.com/GoHyperrr/mdk v0.0.0-20260605044506-3d2ab0d97ca9/go.mod h1:eZBBN0St+r0Gu5K5CklIsS6/SlVx6t2WsvOjkR1t4Ak= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/jwt/jwt.go b/jwt/jwt.go index 427e22f..c7bd271 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/GoHyperrr/auth" "github.com/GoHyperrr/mdk" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" @@ -105,8 +106,8 @@ func (s *AuthStore) DeleteExpiredTokens(ctx context.Context, now time.Time) erro func (s *AuthStore) GenerateToken(actor mdk.Actor) (string, error) { jti := uuid.New().String() claims := Claims{ - ActorID: actor.ID, - ActorType: actor.Type, + ActorID: actor.GetID(), + ActorType: actor.GetType(), RegisteredClaims: jwt.RegisteredClaims{ ID: jti, ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.jwtExpiration)), @@ -119,7 +120,7 @@ func (s *AuthStore) GenerateToken(actor mdk.Actor) (string, error) { } // ValidateToken parses and validates a JWT string. -func (s *AuthStore) ValidateToken(ctx context.Context, tokenString string) (*mdk.Actor, error) { +func (s *AuthStore) ValidateToken(ctx context.Context, tokenString string) (mdk.Actor, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) @@ -141,7 +142,7 @@ func (s *AuthStore) ValidateToken(ctx context.Context, tokenString string) (*mdk return nil, errors.New("token is revoked") } - return &mdk.Actor{ + return &auth.Actor{ ID: claims.ActorID, Type: claims.ActorType, }, nil