mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-04 08:42:36 +02:00
chore: add copyright headers, CI tests, and sanitize gitignore
This commit is contained in:
parent
5cbb3d89d3
commit
d1f844235e
325 changed files with 2267 additions and 902 deletions
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -62,13 +66,13 @@ func SeedDemoTenant(userStore *UserStore, tenantStore *TenantStore, socRepo doms
|
|||
|
||||
// 2. Create demo tenant (demo plan 1000 events max)
|
||||
demoTenant := &Tenant{
|
||||
ID: DemoTenantID,
|
||||
Name: "SYNTREX Demo",
|
||||
Slug: "demo",
|
||||
PlanID: "demo",
|
||||
OwnerUserID: demoUser.ID,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
ID: DemoTenantID,
|
||||
Name: "SYNTREX Demo",
|
||||
Slug: "demo",
|
||||
PlanID: "demo",
|
||||
OwnerUserID: demoUser.ID,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
MonthResetAt: monthStart(time.Now().AddDate(0, 1, 0)),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -31,7 +35,9 @@ func HandleLogin(store *UserStore, secret []byte) http.HandlerFunc {
|
|||
email := req.Email
|
||||
if email == "" {
|
||||
// Try legacy format
|
||||
var legacy struct{ Username string `json:"username"` }
|
||||
var legacy struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
email = legacy.Username
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
// Package auth provides JWT authentication for the SOC HTTP API.
|
||||
// Uses HMAC-SHA256 (HS256) with configurable secret.
|
||||
// Zero external dependencies — pure Go stdlib.
|
||||
|
|
@ -16,21 +20,21 @@ import (
|
|||
|
||||
// Standard JWT errors.
|
||||
var (
|
||||
ErrInvalidToken = errors.New("auth: invalid token")
|
||||
ErrExpiredToken = errors.New("auth: token expired")
|
||||
ErrInvalidSecret = errors.New("auth: secret too short (min 32 bytes)")
|
||||
ErrWrongTokenType = errors.New("auth: wrong token type")
|
||||
ErrInvalidToken = errors.New("auth: invalid token")
|
||||
ErrExpiredToken = errors.New("auth: token expired")
|
||||
ErrInvalidSecret = errors.New("auth: secret too short (min 32 bytes)")
|
||||
ErrWrongTokenType = errors.New("auth: wrong token type")
|
||||
)
|
||||
|
||||
// Claims represents JWT payload.
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"` // Subject (username or user ID)
|
||||
Role string `json:"role"` // RBAC role: admin, operator, analyst, viewer
|
||||
TenantID string `json:"tenant_id,omitempty"` // Multi-tenant isolation
|
||||
Sub string `json:"sub"` // Subject (username or user ID)
|
||||
Role string `json:"role"` // RBAC role: admin, operator, analyst, viewer
|
||||
TenantID string `json:"tenant_id,omitempty"` // Multi-tenant isolation
|
||||
TokenType string `json:"token_type,omitempty"` // "access" or "refresh"
|
||||
Exp int64 `json:"exp"` // Expiration (Unix timestamp)
|
||||
Iat int64 `json:"iat"` // Issued at
|
||||
Iss string `json:"iss,omitempty"` // Issuer
|
||||
Exp int64 `json:"exp"` // Expiration (Unix timestamp)
|
||||
Iat int64 `json:"iat"` // Issued at
|
||||
Iss string `json:"iss,omitempty"` // Issuer
|
||||
}
|
||||
|
||||
// IsExpired returns true if the token has expired.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -37,9 +41,9 @@ func TestSign_Verify_RoundTrip(t *testing.T) {
|
|||
|
||||
func TestVerify_ExpiredToken(t *testing.T) {
|
||||
token, _ := Sign(Claims{
|
||||
Sub: "user",
|
||||
Sub: "user",
|
||||
Role: "viewer",
|
||||
Exp: time.Now().Add(-time.Hour).Unix(),
|
||||
Exp: time.Now().Add(-time.Hour).Unix(),
|
||||
}, testSecret)
|
||||
|
||||
_, err := Verify(token, testSecret)
|
||||
|
|
@ -50,9 +54,9 @@ func TestVerify_ExpiredToken(t *testing.T) {
|
|||
|
||||
func TestVerify_InvalidSignature(t *testing.T) {
|
||||
token, _ := Sign(Claims{
|
||||
Sub: "user",
|
||||
Sub: "user",
|
||||
Role: "viewer",
|
||||
Exp: time.Now().Add(time.Hour).Unix(),
|
||||
Exp: time.Now().Add(time.Hour).Unix(),
|
||||
}, testSecret)
|
||||
|
||||
wrongSecret := []byte("wrong-secret-that-is-also-32-bytes-x")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -13,7 +17,7 @@ const claimsKey ctxKey = "jwt_claims"
|
|||
|
||||
// JWTMiddleware validates Bearer tokens on protected routes.
|
||||
type JWTMiddleware struct {
|
||||
secret []byte
|
||||
secret []byte
|
||||
// PublicPaths are exempt from auth (e.g., /health, /api/auth/login).
|
||||
PublicPaths map[string]bool
|
||||
}
|
||||
|
|
@ -23,23 +27,23 @@ func NewJWTMiddleware(secret []byte) *JWTMiddleware {
|
|||
return &JWTMiddleware{
|
||||
secret: secret,
|
||||
PublicPaths: map[string]bool{
|
||||
"/health": true,
|
||||
"/healthz": true,
|
||||
"/readyz": true,
|
||||
"/metrics": true,
|
||||
"/api/auth/login": true,
|
||||
"/api/auth/logout": true,
|
||||
"/api/auth/refresh": true,
|
||||
"/api/auth/register": true,
|
||||
"/api/auth/verify": true,
|
||||
"/api/auth/plans": true,
|
||||
"/api/auth/demo": true,
|
||||
"/api/v1/scan": true, // public demo scanner
|
||||
"/api/v1/usage": true, // public usage/quota check
|
||||
"/api/v1/soc/events": true, // sensor ingest (auth via RBAC API key when enabled)
|
||||
"/health": true,
|
||||
"/healthz": true,
|
||||
"/readyz": true,
|
||||
"/metrics": true,
|
||||
"/api/auth/login": true,
|
||||
"/api/auth/logout": true,
|
||||
"/api/auth/refresh": true,
|
||||
"/api/auth/register": true,
|
||||
"/api/auth/verify": true,
|
||||
"/api/auth/plans": true,
|
||||
"/api/auth/demo": true,
|
||||
"/api/v1/scan": true, // public demo scanner
|
||||
"/api/v1/usage": true, // public usage/quota check
|
||||
"/api/v1/soc/events": true, // sensor ingest (auth via RBAC API key when enabled)
|
||||
"/api/soc/events/stream": true, // SSE uses query param auth
|
||||
"/api/soc/stream": true, // SSE live feed (EventSource can't send headers)
|
||||
"/api/soc/ws": true, // WebSocket-style SSE push
|
||||
"/api/soc/stream": true, // SSE live feed (EventSource can't send headers)
|
||||
"/api/soc/ws": true, // WebSocket-style SSE push
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -250,8 +254,8 @@ func HandleGetTenant(tenantStore *TenantStore) http.HandlerFunc {
|
|||
"plan": plan,
|
||||
"usage": map[string]interface{}{
|
||||
"events_this_month": tenant.EventsThisMonth,
|
||||
"events_limit": plan.MaxEventsMonth,
|
||||
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
|
||||
"events_limit": plan.MaxEventsMonth,
|
||||
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -339,13 +343,13 @@ func HandleBillingStatus(tenantStore *TenantStore) http.HandlerFunc {
|
|||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"plan": plan,
|
||||
"plan": plan,
|
||||
"payment_customer_id": tenant.PaymentCustomerID,
|
||||
"payment_sub_id": tenant.PaymentSubID,
|
||||
"events_used": tenant.EventsThisMonth,
|
||||
"events_limit": plan.MaxEventsMonth,
|
||||
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
|
||||
"next_reset": tenant.MonthResetAt,
|
||||
"events_used": tenant.EventsThisMonth,
|
||||
"events_limit": plan.MaxEventsMonth,
|
||||
"usage_percent": usagePercent(tenant.EventsThisMonth, plan.MaxEventsMonth),
|
||||
"next_reset": tenant.MonthResetAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -419,7 +423,7 @@ func HandleListTenants(tenantStore *TenantStore) http.HandlerFunc {
|
|||
}
|
||||
|
||||
tenants := tenantStore.ListTenants()
|
||||
|
||||
|
||||
type tenantResp struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -427,7 +431,7 @@ func HandleListTenants(tenantStore *TenantStore) http.HandlerFunc {
|
|||
PlanID string `json:"plan_id"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
|
||||
res := make([]tenantResp, len(tenants))
|
||||
for i, t := range tenants {
|
||||
res[i] = tenantResp{
|
||||
|
|
@ -471,16 +475,16 @@ func HandleImpersonateTenant(tenantStore *TenantStore, jwtSecret []byte) http.Ha
|
|||
// Issue new token with updated TenantID
|
||||
accessClaims := Claims{
|
||||
Sub: claims.Sub,
|
||||
Role: claims.Role, // Preserves superadmin explicitly
|
||||
Role: claims.Role, // Preserves superadmin explicitly
|
||||
TenantID: req.TenantID, // The impersonated tenant ID
|
||||
TokenType: "access",
|
||||
Exp: time.Now().Add(15 * time.Minute).Unix(),
|
||||
}
|
||||
|
||||
|
||||
refreshClaims := Claims{
|
||||
Sub: claims.Sub,
|
||||
Role: claims.Role,
|
||||
TenantID: req.TenantID,
|
||||
TenantID: req.TenantID,
|
||||
TokenType: "refresh",
|
||||
Exp: time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
}
|
||||
|
|
@ -521,8 +525,8 @@ func HandleImpersonateTenant(tenantStore *TenantStore, jwtSecret []byte) http.Ha
|
|||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "success",
|
||||
"tenant_id": req.TenantID,
|
||||
"status": "success",
|
||||
"tenant_id": req.TenantID,
|
||||
"csrf_token": csrfToken,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -22,17 +26,17 @@ type Plan struct {
|
|||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
MaxUsers int `json:"max_users"`
|
||||
MaxEventsMonth int `json:"max_events_month"` // SOC event ingestion quota (-1=unlimited)
|
||||
MaxEventsMonth int `json:"max_events_month"` // SOC event ingestion quota (-1=unlimited)
|
||||
MaxIncidents int `json:"max_incidents"`
|
||||
MaxSensors int `json:"max_sensors"`
|
||||
MaxScansMonth int `json:"max_scans_month"` // /api/v1/scan quota (-1=unlimited, 0=none)
|
||||
MaxScansMonth int `json:"max_scans_month"` // /api/v1/scan quota (-1=unlimited, 0=none)
|
||||
RetentionDays int `json:"retention_days"`
|
||||
SOCEnabled bool `json:"soc_enabled"` // SOC Dashboard access
|
||||
SOCEnabled bool `json:"soc_enabled"` // SOC Dashboard access
|
||||
SLAEnabled bool `json:"sla_enabled"`
|
||||
SOAREnabled bool `json:"soar_enabled"`
|
||||
ComplianceEnabled bool `json:"compliance_enabled"`
|
||||
OnPremise bool `json:"on_premise"` // Enterprise: on-premise deployment
|
||||
PriceMonthCents int `json:"price_month_cents"` // 0 = free, -1 = custom pricing
|
||||
OnPremise bool `json:"on_premise"` // Enterprise: on-premise deployment
|
||||
PriceMonthCents int `json:"price_month_cents"` // 0 = free, -1 = custom pricing
|
||||
}
|
||||
|
||||
// DefaultPlans defines the standard pricing tiers (prices in RUB kopecks).
|
||||
|
|
@ -40,64 +44,64 @@ var DefaultPlans = map[string]Plan{
|
|||
"free": {
|
||||
ID: "free", Name: "Free",
|
||||
Description: "Scanner API — 1 000 сканов/мес, все 66 движков, без SOC Dashboard",
|
||||
MaxUsers: 1, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 1,
|
||||
MaxUsers: 1, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 1,
|
||||
MaxScansMonth: 1000,
|
||||
RetentionDays: 3,
|
||||
SOCEnabled: false, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
|
||||
SOCEnabled: false, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
|
||||
PriceMonthCents: 0,
|
||||
},
|
||||
"demo": {
|
||||
ID: "demo", Name: "Demo Sandbox",
|
||||
Description: "Общая демо-песочница. Жёсткий лимит.",
|
||||
MaxUsers: 10, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 5,
|
||||
MaxUsers: 10, MaxEventsMonth: 1000, MaxIncidents: 100, MaxSensors: 5,
|
||||
MaxScansMonth: 1000,
|
||||
RetentionDays: 1,
|
||||
SOCEnabled: true, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
|
||||
SOCEnabled: true, SLAEnabled: false, SOAREnabled: false, ComplianceEnabled: false,
|
||||
PriceMonthCents: 0,
|
||||
},
|
||||
"starter": {
|
||||
ID: "starter", Name: "Starter",
|
||||
Description: "AI-мониторинг: до 5 сенсоров, базовая корреляция и алерты",
|
||||
MaxUsers: 10, MaxEventsMonth: 100000, MaxIncidents: 200, MaxSensors: 5,
|
||||
MaxUsers: 10, MaxEventsMonth: 100000, MaxIncidents: 200, MaxSensors: 5,
|
||||
MaxScansMonth: 100000,
|
||||
RetentionDays: 30,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: false, ComplianceEnabled: false,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: false, ComplianceEnabled: false,
|
||||
PriceMonthCents: 8990000, // 89 900 ₽/мес
|
||||
},
|
||||
"professional": {
|
||||
ID: "professional", Name: "Professional",
|
||||
Description: "Полный AI SOC: SOAR, compliance, расширенная аналитика",
|
||||
MaxUsers: 50, MaxEventsMonth: 500000, MaxIncidents: 1000, MaxSensors: 25,
|
||||
MaxUsers: 50, MaxEventsMonth: 500000, MaxIncidents: 1000, MaxSensors: 25,
|
||||
MaxScansMonth: 500000,
|
||||
RetentionDays: 90,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
|
||||
PriceMonthCents: 14990000, // 149 900 ₽/мес
|
||||
},
|
||||
"enterprise": {
|
||||
ID: "enterprise", Name: "Enterprise",
|
||||
Description: "On-premise / выделенный инстанс. Сертификация — на стороне заказчика",
|
||||
MaxUsers: -1, MaxEventsMonth: -1, MaxIncidents: -1, MaxSensors: -1,
|
||||
MaxUsers: -1, MaxEventsMonth: -1, MaxIncidents: -1, MaxSensors: -1,
|
||||
MaxScansMonth: -1, // unlimited
|
||||
RetentionDays: 365,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
|
||||
OnPremise: true,
|
||||
SOCEnabled: true, SLAEnabled: true, SOAREnabled: true, ComplianceEnabled: true,
|
||||
OnPremise: true,
|
||||
PriceMonthCents: -1, // по запросу
|
||||
},
|
||||
}
|
||||
|
||||
// Tenant represents an isolated organization in the multi-tenant system.
|
||||
type Tenant struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
PlanID string `json:"plan_id"`
|
||||
PaymentCustomerID string `json:"payment_customer_id,omitempty"`
|
||||
PaymentSubID string `json:"payment_sub_id,omitempty"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
EventsThisMonth int `json:"events_this_month"`
|
||||
MonthResetAt time.Time `json:"month_reset_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
PlanID string `json:"plan_id"`
|
||||
PaymentCustomerID string `json:"payment_customer_id,omitempty"`
|
||||
PaymentSubID string `json:"payment_sub_id,omitempty"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
EventsThisMonth int `json:"events_this_month"`
|
||||
MonthResetAt time.Time `json:"month_reset_at"`
|
||||
}
|
||||
|
||||
// GetPlan returns the tenant's plan configuration.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
// Copyright 2026 Syntrex Lab. All rights reserved.
|
||||
// Use of this source code is governed by an Apache-2.0 license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
|
@ -67,7 +71,7 @@ func NewUserStore(db ...*sql.DB) *UserStore {
|
|||
// Ensure default admin exists or is updated
|
||||
adminPass := os.Getenv("SYNTREX_ADMIN_PASSWORD")
|
||||
if adminPass == "" {
|
||||
// If no env var, use a secure random password to prevent accidental exposure
|
||||
// If no env var, use a secure random password to prevent accidental exposure
|
||||
// if the database is clean, but do not override an existing admin's password.
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
|
|
@ -408,12 +412,12 @@ func (s *UserStore) DeleteUser(id string) error {
|
|||
|
||||
// APIKey represents an API key for programmatic access.
|
||||
type APIKey struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
KeyPrefix string `json:"key_prefix"` // first 8 chars for display
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
KeyPrefix string `json:"key_prefix"` // first 8 chars for display
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue