diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index 64b6659..587f3d0 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.26 + go-version: '1.26' - name: Install gosec run: go install github.com/securego/gosec/v2/cmd/gosec@latest diff --git a/README.md b/README.md index d9fe6f9..949dca5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ ### Feature in the Development -- Add refresh expired token in the cache ( adding the logic for cache ) \ No newline at end of file +- Add refresh expired token in the cache ( adding the logic for cache ) + + +go run ./cmd/migrate up + +go run ./cmd/migrate down \ No newline at end of file diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go index 1bfe662..1e31926 100644 --- a/cmd/migrate/main.go +++ b/cmd/migrate/main.go @@ -180,7 +180,7 @@ func loadMigrations(dir, direction string) []migration { for _, f := range files { base := filepath.Base(f) name := strings.TrimSuffix(base, "."+direction+".sql") - content, err := os.ReadFile(f) + content, err := os.ReadFile(f) // #nosec G304 -- CLI tool reading its own SQL files, no user input if err != nil { log.Fatalf("ReadFile %q: %v", f, err) } diff --git a/config/config.go b/config/config.go index 51039f4..cf799bf 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ import ( "github.com/streadway/amqp" "github.com/vviveksharma/auth/cache" "github.com/vviveksharma/auth/db" + dbmigrations "github.com/vviveksharma/auth/db/migrations" "github.com/vviveksharma/auth/initsetup" "github.com/vviveksharma/auth/internal/controllers" orgcontrollers "github.com/vviveksharma/auth/internal/controllers/orgControllers" @@ -49,6 +50,20 @@ func InitializeSharedResources() (*SharedResources, error) { log.Println("⚠️ Warning: Error loading .env file", err) } db.ConnectDB() + + // Run all pending SQL migrations before anything else touches the DB. + // On a fresh/wiped DB this creates every table; on a running system it + // is a no-op (already-applied migrations are skipped). + sqlDB, err := db.DB.DB() + if err != nil { + initError = fmt.Errorf("get underlying sql.DB for migrations: %w", err) + return + } + if err := dbmigrations.RunUp(sqlDB); err != nil { + initError = fmt.Errorf("auto-migration failed: %w", err) + return + } + redisClient := cache.ConnectCache() if redisClient == nil { initError = fmt.Errorf("failed to connect to Redis: client is nil") diff --git a/db/db.go b/db/db.go index c52f021..9bb918f 100644 --- a/db/db.go +++ b/db/db.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "strings" "time" "gorm.io/driver/postgres" @@ -14,11 +15,21 @@ var DB *gorm.DB func ConnectDB() { // Get DB host from environment, default to localhost for local development - dbHost := os.Getenv("DB_HOST") + // Strip newlines from env vars to prevent log injection (G706) + stripNL := func(s string) string { + return strings.Map(func(r rune) rune { + if r == '\n' || r == '\r' { + return -1 + } + return r + }, s) + } + + dbHost := stripNL(os.Getenv("DB_HOST")) if dbHost == "" { dbHost = "localhost" } - dbPort := os.Getenv("DB_PORT") + dbPort := stripNL(os.Getenv("DB_PORT")) if dbPort == "" { dbPort = "26257" } diff --git a/db/migrations/runner.go b/db/migrations/runner.go new file mode 100644 index 0000000..5c4b382 --- /dev/null +++ b/db/migrations/runner.go @@ -0,0 +1,144 @@ +package migrations + +import ( + "database/sql" + "embed" + "fmt" + "log" + "sort" + "strings" +) + +//go:embed sql +var sqlFS embed.FS + +const migrationsTable = "schema_migrations" + +// RunUp applies every pending *.up.sql file that has not yet been recorded in +// schema_migrations. SQL files are embedded at compile time so no filesystem +// path is needed — the binary is fully self-contained in Docker or anywhere else. +// It is idempotent and safe to call on every server startup. +func RunUp(db *sql.DB) error { + if err := ensureMigrationsTable(db); err != nil { + return fmt.Errorf("ensure migrations table: %w", err) + } + + files := loadEmbeddedFiles("up") + applied := appliedSet(db) + + ran := 0 + for _, f := range files { + if applied[f.name] { + continue + } + log.Printf("⬆ Applying migration: %s", f.name) + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("begin tx for %s: %w", f.name, err) + } + // Strip any BEGIN/COMMIT/ROLLBACK the file may contain — the runner + // manages the transaction itself, and CockroachDB rejects nested BEGIN. + if _, err := tx.Exec(stripTransactionWrappers(f.sql)); err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + log.Printf("⚠️ rollback failed after migration exec error: %v", rbErr) + } + return fmt.Errorf("execute migration %s: %w", f.name, err) + } + if _, err := tx.Exec( + fmt.Sprintf(`INSERT INTO %s (name) VALUES ($1)`, migrationsTable), + f.name, + ); err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + log.Printf("⚠️ rollback failed after mark-applied error: %v", rbErr) + } + return fmt.Errorf("mark migration %s applied: %w", f.name, err) + } + if err := tx.Commit(); err != nil { + return fmt.Errorf("commit migration %s: %w", f.name, err) + } + log.Printf("✅ Migration applied: %s", f.name) + ran++ + } + + if ran == 0 { + log.Println("✅ All migrations already applied — nothing to do.") + } else { + log.Printf("✅ Applied %d migration(s) successfully.", ran) + } + return nil +} + +type migFile struct { + name string + sql string +} + +func ensureMigrationsTable(db *sql.DB) error { + _, err := db.Exec(fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + name TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() + )`, migrationsTable)) + return err +} + +func loadEmbeddedFiles(direction string) []migFile { + entries, err := sqlFS.ReadDir("sql") + if err != nil { + log.Fatalf("Cannot read embedded migrations: %v", err) + } + + suffix := "." + direction + ".sql" + var files []migFile + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), suffix) { + continue + } + content, err := sqlFS.ReadFile("sql/" + e.Name()) + if err != nil { + log.Fatalf("Cannot read embedded migration file %s: %v", e.Name(), err) + } + // Store without direction suffix so up/down share the same key in schema_migrations + name := strings.TrimSuffix(e.Name(), suffix) + files = append(files, migFile{name: name, sql: string(content)}) + } + + sort.Slice(files, func(i, j int) bool { return files[i].name < files[j].name }) + return files +} + +// stripTransactionWrappers removes bare BEGIN, COMMIT, and ROLLBACK statements +// from SQL text so migration files that include them for use with external +// tools (psql, migrate CLI) can be run safely inside the runner's own tx. +func stripTransactionWrappers(sql string) string { + lines := strings.Split(sql, "\n") + kept := lines[:0] + for _, line := range lines { + trimmed := strings.TrimSpace(strings.ToUpper(line)) + if trimmed == "BEGIN" || trimmed == "BEGIN;" || + trimmed == "COMMIT" || trimmed == "COMMIT;" || + trimmed == "ROLLBACK" || trimmed == "ROLLBACK;" { + continue + } + kept = append(kept, line) + } + return strings.Join(kept, "\n") +} + +func appliedSet(db *sql.DB) map[string]bool { + rows, err := db.Query(fmt.Sprintf(`SELECT name FROM %s`, migrationsTable)) + if err != nil { + log.Fatalf("Query applied migrations: %v", err) + } + defer rows.Close() + + applied := map[string]bool{} + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + log.Fatalf("Scan migration name: %v", err) + } + applied[name] = true + } + return applied +} diff --git a/db/migrations/sql/000014_seed_system_roles.down.sql b/db/migrations/sql/000014_seed_system_roles.down.sql new file mode 100644 index 0000000..1df5fd2 --- /dev/null +++ b/db/migrations/sql/000014_seed_system_roles.down.sql @@ -0,0 +1,11 @@ +-- ============================================================= +-- Migration 000014 DOWN: Remove all gr.* system roles +-- ============================================================= + +BEGIN; + +DELETE FROM route_role_tbl WHERE role_name LIKE 'gr.%'; +DELETE FROM role_tbl WHERE role LIKE 'gr.%'; +DROP FUNCTION IF EXISTS uuid_generate_v7(); + +COMMIT; diff --git a/db/migrations/sql/000014_seed_system_roles.up.sql b/db/migrations/sql/000014_seed_system_roles.up.sql new file mode 100644 index 0000000..c467940 --- /dev/null +++ b/db/migrations/sql/000014_seed_system_roles.up.sql @@ -0,0 +1,362 @@ +-- ============================================================= +-- Migration 000014: Seed gr.* system roles + route-role mapping +-- +-- UUIDs are hardcoded UUIDv7 (time-ordered, sequential). +-- Timestamp prefix 0195948d-c000 ≈ 2026-03-19 UTC +-- +-- System tenant: dae760ab-0a7f-4cbd-8603-def85ad8e430 +-- (no FK constraint on role_tbl, tenant row not required) +-- +-- Replaces: initsetup/initsetup.go GORM-based seeding. +-- Idempotent: all INSERTs use ON CONFLICT (id) DO NOTHING. +-- ============================================================= + +BEGIN; + +-- ============================================================= +-- 1. role_tbl +-- ============================================================= +INSERT INTO role_tbl + (id, role_id, tenant_id, role, display_name, description, role_type, status, created_at, updated_at) +VALUES + -- ── Tenant-level ───────────────────────────────────────── + ('0195948d-c000-7001-8001-000000000001'::UUID, '0195948d-c000-7001-8001-000000000001'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.tenant.owner', 'Account Owner', + 'Full account control: users, orgs, billing, deletion, and all administrative actions.', + 'default', true, now(), now()), + + ('0195948d-c000-7002-8001-000000000002'::UUID, '0195948d-c000-7002-8001-000000000002'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.tenant.admin', 'Account Administrator', + 'Manages users, orgs, roles and tokens. No billing or account deletion.', + 'default', true, now(), now()), + + ('0195948d-c000-7003-8001-000000000003'::UUID, '0195948d-c000-7003-8001-000000000003'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.tenant.auditor', 'Account Auditor', + 'Read-only visibility across the full tenant. Cannot modify anything.', + 'default', true, now(), now()), + + -- ── User-level ─────────────────────────────────────────── + ('0195948d-c000-7010-8001-000000000010'::UUID, '0195948d-c000-7010-8001-000000000010'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.user', 'User', + 'Standard authenticated user. Self-service access, can request to join organizations.', + 'default', true, now(), now()), + + ('0195948d-c000-7011-8001-000000000011'::UUID, '0195948d-c000-7011-8001-000000000011'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.guest', 'Guest', + 'Unauthenticated visitor. Can only register and login.', + 'default', true, now(), now()), + + -- ── Org-level ──────────────────────────────────────────── + ('0195948d-c001-7001-8002-000000000021'::UUID, '0195948d-c001-7001-8002-000000000021'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.org.owner', 'Organization Owner', + 'Created the organization. Full control including deletion and all member management.', + 'default', true, now(), now()), + + ('0195948d-c001-7002-8002-000000000022'::UUID, '0195948d-c001-7002-8002-000000000022'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.org.lead', 'Organization Lead', + 'Approves/rejects join requests, manages members, updates org settings. Cannot delete org.', + 'default', true, now(), now()), + + ('0195948d-c001-7003-8002-000000000023'::UUID, '0195948d-c001-7003-8002-000000000023'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.org.member', 'Member', + 'Standard organization contributor.', + 'default', true, now(), now()), + + ('0195948d-c001-7004-8002-000000000024'::UUID, '0195948d-c001-7004-8002-000000000024'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.org.observer', 'Observer', + 'Read-only access to the organization. Cannot modify members or settings.', + 'default', true, now(), now()), + + -- ── Platform root ───────────────────────────────────────── + ('0195948d-c002-7001-8003-000000000031'::UUID, '0195948d-c002-7001-8003-000000000031'::UUID, + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + 'gr.system.root', 'Platform Root', + 'Internal GuardRail system identity. Never assignable to human users.', + 'default', true, now(), now()) + +ON CONFLICT (id) DO NOTHING; + +-- ============================================================= +-- 2. route_role_tbl +-- ============================================================= + +-- gr.tenant.owner (priority 100) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7001-8004-000000000041'::UUID, 'gr.tenant.owner', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c000-7001-8001-000000000001'::UUID, + '{ + "role_info": {"name":"gr.tenant.owner","display_name":"Account Owner","role_type":"default","priority":100,"is_system":true}, + "permissions": [ + {"route":"/auth/", "methods":["POST"], "description":"Register user"}, + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"Own profile"}, + {"route":"/users/:id", "methods":["GET","PUT","DELETE"], "description":"Full user management"}, + {"route":"/users", "methods":["GET","POST"], "description":"List and create users"}, + {"route":"/users/:id/roles", "methods":["PUT"], "description":"Assign roles to users"}, + {"route":"/user/resetpassword", "methods":["POST"], "description":"Reset passwords"}, + {"route":"/user/setpassword", "methods":["PUT"], "description":"Set passwords"}, + {"route":"/roles", "methods":["GET","POST"], "description":"List and create roles"}, + {"route":"/roles/:id", "methods":["GET","DELETE"], "description":"Role detail and delete"}, + {"route":"/roles/:id/permissions", "methods":["GET","PUT"], "description":"Manage role permissions"}, + {"route":"/roles/enable/:id", "methods":["PUT"], "description":"Enable role"}, + {"route":"/roles/disable/:id", "methods":["PUT"], "description":"Disable role"}, + {"route":"/request", "methods":["POST","GET"], "description":"Role assignment requests"}, + {"route":"/request/status", "methods":["GET"], "description":"Request status"}, + {"route":"/organizations", "methods":["GET","POST"], "description":"List and create orgs"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search discoverable orgs"}, + {"route":"/organizations/:id", "methods":["GET","PUT","DELETE"], "description":"Full org CRUD"}, + {"route":"/organizations/:id/switch", "methods":["POST"], "description":"Switch active org"}, + {"route":"/organizations/:id/join-requests", "methods":["GET","POST"], "description":"Submit and list join requests"}, + {"route":"/organizations/:id/join-requests/:reqId", "methods":["GET"], "description":"Join request detail"}, + {"route":"/organizations/:id/join-requests/:reqId/approve", "methods":["POST"], "description":"Approve join request"}, + {"route":"/organizations/:id/join-requests/:reqId/reject", "methods":["POST"], "description":"Reject join request"}, + {"route":"/organizations/:id/discovery-settings", "methods":["PUT"], "description":"Update discovery settings"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/','/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/users/:id/roles','/user/resetpassword','/user/setpassword','/roles','/roles/:id','/roles/:id/permissions','/roles/enable/:id','/roles/disable/:id','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.tenant.admin (priority 90) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7002-8004-000000000042'::UUID, 'gr.tenant.admin', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c000-7002-8001-000000000002'::UUID, + '{ + "role_info": {"name":"gr.tenant.admin","display_name":"Account Administrator","role_type":"default","priority":90,"is_system":true}, + "permissions": [ + {"route":"/auth/", "methods":["POST"], "description":"Register user"}, + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"Own profile"}, + {"route":"/users/:id", "methods":["GET","PUT","DELETE"], "description":"Full user management"}, + {"route":"/users", "methods":["GET","POST"], "description":"List and create users"}, + {"route":"/users/:id/roles", "methods":["PUT"], "description":"Assign roles to users"}, + {"route":"/user/resetpassword", "methods":["POST"], "description":"Reset passwords"}, + {"route":"/user/setpassword", "methods":["PUT"], "description":"Set passwords"}, + {"route":"/roles", "methods":["GET","POST"], "description":"List and create roles"}, + {"route":"/roles/:id", "methods":["GET","DELETE"], "description":"Role detail and delete"}, + {"route":"/roles/:id/permissions", "methods":["GET","PUT"], "description":"Manage role permissions"}, + {"route":"/roles/enable/:id", "methods":["PUT"], "description":"Enable role"}, + {"route":"/roles/disable/:id", "methods":["PUT"], "description":"Disable role"}, + {"route":"/request", "methods":["POST","GET"], "description":"Role assignment requests"}, + {"route":"/request/status", "methods":["GET"], "description":"Request status"}, + {"route":"/organizations", "methods":["GET","POST"], "description":"List and create orgs"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET","PUT"], "description":"View and update org (no delete)"}, + {"route":"/organizations/:id/switch", "methods":["POST"], "description":"Switch active org"}, + {"route":"/organizations/:id/join-requests", "methods":["GET","POST"], "description":"Submit and list join requests"}, + {"route":"/organizations/:id/join-requests/:reqId", "methods":["GET"], "description":"Join request detail"}, + {"route":"/organizations/:id/join-requests/:reqId/approve", "methods":["POST"], "description":"Approve join request"}, + {"route":"/organizations/:id/join-requests/:reqId/reject", "methods":["POST"], "description":"Reject join request"}, + {"route":"/organizations/:id/discovery-settings", "methods":["PUT"], "description":"Update discovery settings"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/','/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/users/:id/roles','/user/resetpassword','/user/setpassword','/roles','/roles/:id','/roles/:id/permissions','/roles/enable/:id','/roles/disable/:id','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.tenant.auditor (priority 70) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7003-8004-000000000043'::UUID, 'gr.tenant.auditor', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c000-7003-8001-000000000003'::UUID, + '{ + "role_info": {"name":"gr.tenant.auditor","display_name":"Account Auditor","role_type":"default","priority":70,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET"], "description":"View own profile"}, + {"route":"/users/:id", "methods":["GET"], "description":"View any user"}, + {"route":"/users", "methods":["GET"], "description":"List users"}, + {"route":"/roles", "methods":["GET"], "description":"List roles"}, + {"route":"/roles/:id", "methods":["GET"], "description":"View role detail"}, + {"route":"/roles/:id/permissions", "methods":["GET"], "description":"View permissions"}, + {"route":"/request", "methods":["GET"], "description":"View role requests"}, + {"route":"/request/status", "methods":["GET"], "description":"View request status"}, + {"route":"/organizations", "methods":["GET"], "description":"List orgs"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET"], "description":"View org detail"}, + {"route":"/organizations/:id/join-requests", "methods":["GET"], "description":"View join requests"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/me/notifications", "methods":["GET"], "description":"View notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/roles','/roles/:id','/roles/:id/permissions','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/join-requests','/me/join-requests','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.user (priority 10) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7010-8004-000000000050'::UUID, 'gr.user', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c000-7010-8001-000000000010'::UUID, + '{ + "role_info": {"name":"gr.user","display_name":"User","role_type":"default","priority":10,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"View and update own profile"}, + {"route":"/user/resetpassword", "methods":["POST"], "description":"Reset own password"}, + {"route":"/user/setpassword", "methods":["PUT"], "description":"Set own password"}, + {"route":"/request", "methods":["POST","GET"], "description":"Submit role assignment request"}, + {"route":"/request/status", "methods":["GET"], "description":"Check request status"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search discoverable orgs"}, + {"route":"/organizations/:id", "methods":["GET"], "description":"View org detail"}, + {"route":"/organizations/:id/join-requests", "methods":["POST"], "description":"Submit join request"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"View own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/user/resetpassword','/user/setpassword','/request','/request/status','/organizations/search','/organizations/:id','/organizations/:id/join-requests','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.guest (priority 0) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7011-8004-000000000051'::UUID, 'gr.guest', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c000-7011-8001-000000000011'::UUID, + '{ + "role_info": {"name":"gr.guest","display_name":"Guest","role_type":"default","priority":0,"is_system":true}, + "permissions": [ + {"route":"/auth/", "methods":["POST"], "description":"Register new account"}, + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/organizations/search","methods":["GET"], "description":"Browse orgs before registering"} + ] + }'::jsonb, + ARRAY['/auth/','/auth/login','/organizations/search'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.org.owner (priority 80) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7021-8004-000000000061'::UUID, 'gr.org.owner', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c001-7001-8002-000000000021'::UUID, + '{ + "role_info": {"name":"gr.org.owner","display_name":"Organization Owner","role_type":"default","priority":80,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"Own profile"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET","PUT","DELETE"], "description":"Full org CRUD incl. delete"}, + {"route":"/organizations/:id/switch", "methods":["POST"], "description":"Switch active org"}, + {"route":"/organizations/:id/join-requests", "methods":["GET"], "description":"List join requests"}, + {"route":"/organizations/:id/join-requests/:reqId", "methods":["GET"], "description":"Join request detail"}, + {"route":"/organizations/:id/join-requests/:reqId/approve", "methods":["POST"], "description":"Approve join request"}, + {"route":"/organizations/:id/join-requests/:reqId/reject", "methods":["POST"], "description":"Reject join request"}, + {"route":"/organizations/:id/discovery-settings", "methods":["PUT"], "description":"Update discovery settings"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.org.lead (priority 60) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7022-8004-000000000062'::UUID, 'gr.org.lead', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c001-7002-8002-000000000022'::UUID, + '{ + "role_info": {"name":"gr.org.lead","display_name":"Organization Lead","role_type":"default","priority":60,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"Own profile"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET","PUT"], "description":"View and update org (no delete)"}, + {"route":"/organizations/:id/switch", "methods":["POST"], "description":"Switch active org"}, + {"route":"/organizations/:id/join-requests", "methods":["GET"], "description":"List join requests"}, + {"route":"/organizations/:id/join-requests/:reqId", "methods":["GET"], "description":"Join request detail"}, + {"route":"/organizations/:id/join-requests/:reqId/approve", "methods":["POST"], "description":"Approve join request"}, + {"route":"/organizations/:id/join-requests/:reqId/reject", "methods":["POST"], "description":"Reject join request"}, + {"route":"/organizations/:id/discovery-settings", "methods":["PUT"], "description":"Update discovery settings"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.org.member (priority 30) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7023-8004-000000000063'::UUID, 'gr.org.member', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c001-7003-8002-000000000023'::UUID, + '{ + "role_info": {"name":"gr.org.member","display_name":"Member","role_type":"default","priority":30,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET","PUT"], "description":"Own profile"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET"], "description":"View org detail"}, + {"route":"/organizations/:id/switch", "methods":["POST"], "description":"Switch active org"}, + {"route":"/me/join-requests", "methods":["GET"], "description":"Own join requests"}, + {"route":"/join-requests/:id", "methods":["DELETE"], "description":"Cancel own join request"}, + {"route":"/me/notifications", "methods":["GET","PUT"], "description":"Notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/me/join-requests','/join-requests/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.org.observer (priority 10) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7024-8004-000000000064'::UUID, 'gr.org.observer', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c001-7004-8002-000000000024'::UUID, + '{ + "role_info": {"name":"gr.org.observer","display_name":"Observer","role_type":"default","priority":10,"is_system":true}, + "permissions": [ + {"route":"/auth/login", "methods":["POST"], "description":"Login"}, + {"route":"/auth/logout", "methods":["PUT"], "description":"Logout"}, + {"route":"/auth/refresh", "methods":["PUT"], "description":"Refresh token"}, + {"route":"/users/me", "methods":["GET"], "description":"View own profile"}, + {"route":"/organizations/search", "methods":["GET"], "description":"Search orgs"}, + {"route":"/organizations/:id", "methods":["GET"], "description":"View org detail"}, + {"route":"/me/notifications", "methods":["GET"], "description":"View notifications"} + ] + }'::jsonb, + ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/me/notifications'] +) ON CONFLICT (id) DO NOTHING; + +-- gr.system.root (priority 999 — never assign to humans) +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES ( + '0195948d-c003-7031-8004-000000000071'::UUID, 'gr.system.root', + 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, + '0195948d-c002-7001-8003-000000000031'::UUID, + '{ + "role_info": {"name":"gr.system.root","display_name":"Platform Root","role_type":"default","priority":999,"is_system":true}, + "permissions": [ + {"route":"*","methods":["GET","POST","PUT","DELETE","PATCH"],"description":"Unrestricted platform access — internal use only"} + ] + }'::jsonb, + ARRAY['*'] +) ON CONFLICT (id) DO NOTHING; + +COMMIT; diff --git a/docker-compose.yaml b/docker-compose.yaml index 57328b7..36db01e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -55,24 +55,6 @@ services: networks: - auth-network - # Shared Mail Server - Mailpit - mailpit: - image: axllent/mailpit - container_name: auth-mailpit - restart: always - ports: - - "1025:1025" # SMTP - - "8025:8025" # Web UI - environment: - - MP_SMTP_AUTH_ALLOW_INSECURE=true - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8025"] - interval: 10s - timeout: 5s - retries: 3 - networks: - - auth-network - # API Server (Port 8080) - For Application Key Clients api-server: build: @@ -86,8 +68,6 @@ services: - DB_HOST=auth-database - DB_PORT=26257 - REDIS_ADDR=auth-redis:6379 - - SMTP_HOST=auth-mailpit - - SMTP_PORT=1025 - RABBITMQ_URL=amqp://user:password@auth-rabbitmq:5672/ depends_on: db: @@ -96,8 +76,6 @@ services: condition: service_healthy rabbitmq: condition: service_healthy - mailpit: - condition: service_healthy networks: - auth-network restart: unless-stopped @@ -121,8 +99,6 @@ services: - DB_HOST=auth-database - DB_PORT=26257 - REDIS_ADDR=auth-redis:6379 - - SMTP_HOST=auth-mailpit - - SMTP_PORT=1025 - RABBITMQ_URL=amqp://user:password@\:5672/ depends_on: db: @@ -131,8 +107,6 @@ services: condition: service_healthy rabbitmq: condition: service_healthy - mailpit: - condition: service_healthy networks: - auth-network restart: unless-stopped diff --git a/initsetup/initsetup.go b/initsetup/initsetup.go index 37ae928..0ed9b1b 100644 --- a/initsetup/initsetup.go +++ b/initsetup/initsetup.go @@ -1,133 +1,58 @@ package initsetup import ( - "fmt" + _ "embed" "log" - "github.com/google/uuid" "github.com/vviveksharma/auth/db" - "github.com/vviveksharma/auth/internal/repo" - "github.com/vviveksharma/auth/internal/utils" - "github.com/vviveksharma/auth/models" - "gorm.io/gorm" ) -// Predefined role IDs -var ( - AdminId = uuid.MustParse("f47ac10b-58cc-4372-a567-0e02b2c3d479") - UserId = uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8") - GuestId = uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") - ModeratorId = uuid.MustParse("1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed") - TenantId = uuid.MustParse("dae760ab-0a7f-4cbd-8603-def85ad8e430") - requiredRoles = []models.DBRoles{ - {Role: "admin", RoleId: AdminId, RoleType: "default", TenantId: TenantId, DisplayName: "Administrator", Status: true}, - {Role: "user", RoleId: UserId, RoleType: "default", TenantId: TenantId, DisplayName: "Content Moderator", Status: true}, - {Role: "guest", RoleId: GuestId, RoleType: "default", TenantId: TenantId, DisplayName: "Standard User", Status: true}, - {Role: "moderator", RoleId: ModeratorId, RoleType: "default", TenantId: TenantId, DisplayName: "Guest User", Status: true}, - } -) +//go:embed seed_system_roles.sql +var seedSQL string -var ( - requiredRolesRoutes = []models.DBRouteRole{ - {TenantId: uuid.MustParse("dae760ab-0a7f-4cbd-8603-def85ad8e430"), RoleId: uuid.MustParse("f47ac10b-58cc-4372-a567-0e02b2c3d479"), RoleName: "admin"}, - {TenantId: uuid.MustParse("dae760ab-0a7f-4cbd-8603-def85ad8e430"), RoleId: uuid.MustParse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), RoleName: "user"}, - {TenantId: uuid.MustParse("dae760ab-0a7f-4cbd-8603-def85ad8e430"), RoleId: uuid.MustParse("1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"), RoleName: "moderator"}, - {TenantId: uuid.MustParse("dae760ab-0a7f-4cbd-8603-def85ad8e430"), RoleId: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000"), RoleName: "guest"}, - } -) +// systemRoleCount is the total number of gr.* roles defined in seed_system_roles.sql. +// Update this constant whenever a role is added or removed from that file. +const systemRoleCount = 10 +// InitSetup ensures all gr.* system roles and their route-role mappings are present. +// It uses a single COUNT check to decide whether seeding is needed, then executes +// the embedded SQL in one transaction. All INSERTs use ON CONFLICT DO NOTHING so +// this is safe to run on every startup. func InitSetup() { - db := db.DB - exist, err := CheckRolesExist(db) + sqlDB, err := db.DB.DB() if err != nil { - log.Fatal("error checking roles existence: ", err) + log.Fatal("initsetup: failed to get underlying sql.DB: ", err) } - var count int64 - err = db.Model(&models.DBRouteRole{}).Count(&count).Error - if err != nil { - log.Fatalln("error while getting the route and role: " + err.Error()) - } - if count >= int64(len(requiredRolesRoutes)) { - log.Println("Routes and role mapping already present") - return - } else { - err = UpdateRoleRoutePermissions() - if err != nil { - log.Fatalln("error while updating the route and role mapping: " + err.Error()) - } + var count int + if err := sqlDB.QueryRow( + `SELECT COUNT(*) FROM role_tbl WHERE role LIKE 'gr.%'`, + ).Scan(&count); err != nil { + log.Fatal("initsetup: failed to count system roles: ", err) } - if exist { - log.Println("roles already exist - skipping creation") + if count >= systemRoleCount { + log.Printf("✅ initsetup: %d system roles already present — skipping seed", count) return } - err = db.Transaction(func(tx *gorm.DB) error { - for _, role := range requiredRoles { - if err := tx.Create(&role).Error; err != nil { - return fmt.Errorf("failed to create role %s: %w", role.Role, err) - } - } - return nil - }) - - if err != nil { - log.Fatal("error creating roles: ", err) - } - log.Println("roles created successfully and mapped to respective routes") -} + log.Printf("🌱 initsetup: found %d/%d system roles — running seed", count, systemRoleCount) -func CheckRolesExist(db *gorm.DB) (bool, error) { - // Count the roles - var count int64 - err := db.Model(&models.DBRoles{}).Count(&count).Error + tx, err := sqlDB.Begin() if err != nil { - return false, err - } - - leng := len(requiredRoles) - - if count >= int64(leng) { - return true, nil + log.Fatal("initsetup: failed to begin transaction: ", err) } - for _, role := range requiredRoles { - var existing models.DBRoles - err := db.Where("role_id = ?", role.RoleId).First(&existing).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return false, nil - } - return false, err + if _, err := tx.Exec(seedSQL); err != nil { + if rbErr := tx.Rollback(); rbErr != nil { + log.Printf("⚠️ initsetup: rollback failed: %v", rbErr) } + log.Fatal("initsetup: seed SQL failed: ", err) } - return true, nil -} -func UpdateRoleRoutePermissions() error { - log.Println("Inside the update permissions") - roleRoute, err := repo.NewRouteRoleRepository(db.DB) - if err != nil { - log.Fatalf("error while connecting to the role route repositery: %s", err.Error()) - return err + if err := tx.Commit(); err != nil { + log.Fatal("initsetup: failed to commit seed transaction: ", err) } - for _, rr := range requiredRolesRoutes { - permissions, err := utils.ReadPermissionFile(rr.RoleName) - if err != nil { - log.Fatalln("error while reding the permissions file: " + err.Error()) - return err - } - err = roleRoute.Create(&models.DBRouteRole{ - RoleName: rr.RoleName, - TenantId: rr.TenantId, - RoleId: rr.RoleId, - Permissions: permissions, - }) - if err != nil { - log.Fatalln("error while creating the roleRoute permission entry: " + err.Error()) - return err - } - } - return nil + + log.Println("✅ initsetup: system roles seeded successfully") } diff --git a/initsetup/seed_system_roles.sql b/initsetup/seed_system_roles.sql new file mode 100644 index 0000000..938b9fd --- /dev/null +++ b/initsetup/seed_system_roles.sql @@ -0,0 +1,31 @@ +-- gr.* system roles seed +-- Executed by initsetup on every server start (ON CONFLICT DO NOTHING = idempotent). +-- DO NOT add BEGIN/COMMIT here — the Go caller wraps this in a single transaction. + +INSERT INTO role_tbl + (id, role_id, tenant_id, role, display_name, description, role_type, status, created_at, updated_at) +VALUES + ('0195948d-c000-7001-8001-000000000001'::UUID, '0195948d-c000-7001-8001-000000000001'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.tenant.owner', 'Account Owner', 'Full account control: users, orgs, billing, deletion, and all administrative actions.', 'default', true, now(), now()), + ('0195948d-c000-7002-8001-000000000002'::UUID, '0195948d-c000-7002-8001-000000000002'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.tenant.admin', 'Account Administrator', 'Manages users, orgs, roles and tokens. No billing or account deletion.', 'default', true, now(), now()), + ('0195948d-c000-7003-8001-000000000003'::UUID, '0195948d-c000-7003-8001-000000000003'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.tenant.auditor', 'Account Auditor', 'Read-only visibility across the full tenant. Cannot modify anything.', 'default', true, now(), now()), + ('0195948d-c000-7010-8001-000000000010'::UUID, '0195948d-c000-7010-8001-000000000010'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.user', 'User', 'Standard authenticated user. Self-service access, can request to join organizations.', 'default', true, now(), now()), + ('0195948d-c000-7011-8001-000000000011'::UUID, '0195948d-c000-7011-8001-000000000011'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.guest', 'Guest', 'Unauthenticated visitor. Can only register and login.', 'default', true, now(), now()), + ('0195948d-c001-7001-8002-000000000021'::UUID, '0195948d-c001-7001-8002-000000000021'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.org.owner', 'Organization Owner', 'Created the organization. Full control including deletion and all member management.', 'default', true, now(), now()), + ('0195948d-c001-7002-8002-000000000022'::UUID, '0195948d-c001-7002-8002-000000000022'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.org.lead', 'Organization Lead', 'Approves/rejects join requests, manages members, updates org settings. Cannot delete org.', 'default', true, now(), now()), + ('0195948d-c001-7003-8002-000000000023'::UUID, '0195948d-c001-7003-8002-000000000023'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.org.member', 'Member', 'Standard organization contributor.', 'default', true, now(), now()), + ('0195948d-c001-7004-8002-000000000024'::UUID, '0195948d-c001-7004-8002-000000000024'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.org.observer', 'Observer', 'Read-only access to the organization. Cannot modify members or settings.', 'default', true, now(), now()), + ('0195948d-c002-7001-8003-000000000031'::UUID, '0195948d-c002-7001-8003-000000000031'::UUID, 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, 'gr.system.root', 'Platform Root', 'Internal GuardRail system identity. Never assignable to human users.', 'default', true, now(), now()) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO route_role_tbl (id, role_name, tenant_id, role_id, permissions, routes) VALUES + ('0195948d-c003-7001-8004-000000000041'::UUID, 'gr.tenant.owner', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c000-7001-8001-000000000001'::UUID, '{"role_info":{"name":"gr.tenant.owner","display_name":"Account Owner","role_type":"default","priority":100,"is_system":true},"permissions":[{"route":"/auth/","methods":["POST"],"description":"Register user"},{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"Own profile"},{"route":"/users/:id","methods":["GET","PUT","DELETE"],"description":"Full user management"},{"route":"/users","methods":["GET","POST"],"description":"List and create users"},{"route":"/users/:id/roles","methods":["PUT"],"description":"Assign roles to users"},{"route":"/user/resetpassword","methods":["POST"],"description":"Reset passwords"},{"route":"/user/setpassword","methods":["PUT"],"description":"Set passwords"},{"route":"/roles","methods":["GET","POST"],"description":"List and create roles"},{"route":"/roles/:id","methods":["GET","DELETE"],"description":"Role detail and delete"},{"route":"/roles/:id/permissions","methods":["GET","PUT"],"description":"Manage role permissions"},{"route":"/roles/enable/:id","methods":["PUT"],"description":"Enable role"},{"route":"/roles/disable/:id","methods":["PUT"],"description":"Disable role"},{"route":"/request","methods":["POST","GET"],"description":"Role assignment requests"},{"route":"/request/status","methods":["GET"],"description":"Request status"},{"route":"/organizations","methods":["GET","POST"],"description":"List and create orgs"},{"route":"/organizations/search","methods":["GET"],"description":"Search discoverable orgs"},{"route":"/organizations/:id","methods":["GET","PUT","DELETE"],"description":"Full org CRUD"},{"route":"/organizations/:id/switch","methods":["POST"],"description":"Switch active org"},{"route":"/organizations/:id/join-requests","methods":["GET","POST"],"description":"Submit and list join requests"},{"route":"/organizations/:id/join-requests/:reqId","methods":["GET"],"description":"Join request detail"},{"route":"/organizations/:id/join-requests/:reqId/approve","methods":["POST"],"description":"Approve join request"},{"route":"/organizations/:id/join-requests/:reqId/reject","methods":["POST"],"description":"Reject join request"},{"route":"/organizations/:id/discovery-settings","methods":["PUT"],"description":"Update discovery settings"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/','/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/users/:id/roles','/user/resetpassword','/user/setpassword','/roles','/roles/:id','/roles/:id/permissions','/roles/enable/:id','/roles/disable/:id','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7002-8004-000000000042'::UUID, 'gr.tenant.admin', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c000-7002-8001-000000000002'::UUID, '{"role_info":{"name":"gr.tenant.admin","display_name":"Account Administrator","role_type":"default","priority":90,"is_system":true},"permissions":[{"route":"/auth/","methods":["POST"],"description":"Register user"},{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"Own profile"},{"route":"/users/:id","methods":["GET","PUT","DELETE"],"description":"Full user management"},{"route":"/users","methods":["GET","POST"],"description":"List and create users"},{"route":"/users/:id/roles","methods":["PUT"],"description":"Assign roles to users"},{"route":"/user/resetpassword","methods":["POST"],"description":"Reset passwords"},{"route":"/user/setpassword","methods":["PUT"],"description":"Set passwords"},{"route":"/roles","methods":["GET","POST"],"description":"List and create roles"},{"route":"/roles/:id","methods":["GET","DELETE"],"description":"Role detail and delete"},{"route":"/roles/:id/permissions","methods":["GET","PUT"],"description":"Manage role permissions"},{"route":"/roles/enable/:id","methods":["PUT"],"description":"Enable role"},{"route":"/roles/disable/:id","methods":["PUT"],"description":"Disable role"},{"route":"/request","methods":["POST","GET"],"description":"Role assignment requests"},{"route":"/request/status","methods":["GET"],"description":"Request status"},{"route":"/organizations","methods":["GET","POST"],"description":"List and create orgs"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET","PUT"],"description":"View and update org (no delete)"},{"route":"/organizations/:id/switch","methods":["POST"],"description":"Switch active org"},{"route":"/organizations/:id/join-requests","methods":["GET","POST"],"description":"Submit and list join requests"},{"route":"/organizations/:id/join-requests/:reqId","methods":["GET"],"description":"Join request detail"},{"route":"/organizations/:id/join-requests/:reqId/approve","methods":["POST"],"description":"Approve join request"},{"route":"/organizations/:id/join-requests/:reqId/reject","methods":["POST"],"description":"Reject join request"},{"route":"/organizations/:id/discovery-settings","methods":["PUT"],"description":"Update discovery settings"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/','/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/users/:id/roles','/user/resetpassword','/user/setpassword','/roles','/roles/:id','/roles/:id/permissions','/roles/enable/:id','/roles/disable/:id','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7003-8004-000000000043'::UUID, 'gr.tenant.auditor', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c000-7003-8001-000000000003'::UUID, '{"role_info":{"name":"gr.tenant.auditor","display_name":"Account Auditor","role_type":"default","priority":70,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET"],"description":"View own profile"},{"route":"/users/:id","methods":["GET"],"description":"View any user"},{"route":"/users","methods":["GET"],"description":"List users"},{"route":"/roles","methods":["GET"],"description":"List roles"},{"route":"/roles/:id","methods":["GET"],"description":"View role detail"},{"route":"/roles/:id/permissions","methods":["GET"],"description":"View permissions"},{"route":"/request","methods":["GET"],"description":"View role requests"},{"route":"/request/status","methods":["GET"],"description":"View request status"},{"route":"/organizations","methods":["GET"],"description":"List orgs"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET"],"description":"View org detail"},{"route":"/organizations/:id/join-requests","methods":["GET"],"description":"View join requests"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/me/notifications","methods":["GET"],"description":"View notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/users/:id','/users','/roles','/roles/:id','/roles/:id/permissions','/request','/request/status','/organizations','/organizations/search','/organizations/:id','/organizations/:id/join-requests','/me/join-requests','/me/notifications']), + ('0195948d-c003-7010-8004-000000000050'::UUID, 'gr.user', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c000-7010-8001-000000000010'::UUID, '{"role_info":{"name":"gr.user","display_name":"User","role_type":"default","priority":10,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"View and update own profile"},{"route":"/user/resetpassword","methods":["POST"],"description":"Reset own password"},{"route":"/user/setpassword","methods":["PUT"],"description":"Set own password"},{"route":"/request","methods":["POST","GET"],"description":"Submit role assignment request"},{"route":"/request/status","methods":["GET"],"description":"Check request status"},{"route":"/organizations/search","methods":["GET"],"description":"Search discoverable orgs"},{"route":"/organizations/:id","methods":["GET"],"description":"View org detail"},{"route":"/organizations/:id/join-requests","methods":["POST"],"description":"Submit join request"},{"route":"/me/join-requests","methods":["GET"],"description":"View own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/user/resetpassword','/user/setpassword','/request','/request/status','/organizations/search','/organizations/:id','/organizations/:id/join-requests','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7011-8004-000000000051'::UUID, 'gr.guest', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c000-7011-8001-000000000011'::UUID, '{"role_info":{"name":"gr.guest","display_name":"Guest","role_type":"default","priority":0,"is_system":true},"permissions":[{"route":"/auth/","methods":["POST"],"description":"Register new account"},{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/organizations/search","methods":["GET"],"description":"Browse orgs before registering"}]}'::jsonb, ARRAY['/auth/','/auth/login','/organizations/search']), + ('0195948d-c003-7021-8004-000000000061'::UUID, 'gr.org.owner', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c001-7001-8002-000000000021'::UUID, '{"role_info":{"name":"gr.org.owner","display_name":"Organization Owner","role_type":"default","priority":80,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"Own profile"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET","PUT","DELETE"],"description":"Full org CRUD incl. delete"},{"route":"/organizations/:id/switch","methods":["POST"],"description":"Switch active org"},{"route":"/organizations/:id/join-requests","methods":["GET"],"description":"List join requests"},{"route":"/organizations/:id/join-requests/:reqId","methods":["GET"],"description":"Join request detail"},{"route":"/organizations/:id/join-requests/:reqId/approve","methods":["POST"],"description":"Approve join request"},{"route":"/organizations/:id/join-requests/:reqId/reject","methods":["POST"],"description":"Reject join request"},{"route":"/organizations/:id/discovery-settings","methods":["PUT"],"description":"Update discovery settings"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7022-8004-000000000062'::UUID, 'gr.org.lead', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c001-7002-8002-000000000022'::UUID, '{"role_info":{"name":"gr.org.lead","display_name":"Organization Lead","role_type":"default","priority":60,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"Own profile"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET","PUT"],"description":"View and update org (no delete)"},{"route":"/organizations/:id/switch","methods":["POST"],"description":"Switch active org"},{"route":"/organizations/:id/join-requests","methods":["GET"],"description":"List join requests"},{"route":"/organizations/:id/join-requests/:reqId","methods":["GET"],"description":"Join request detail"},{"route":"/organizations/:id/join-requests/:reqId/approve","methods":["POST"],"description":"Approve join request"},{"route":"/organizations/:id/join-requests/:reqId/reject","methods":["POST"],"description":"Reject join request"},{"route":"/organizations/:id/discovery-settings","methods":["PUT"],"description":"Update discovery settings"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/organizations/:id/join-requests','/organizations/:id/join-requests/:reqId','/organizations/:id/join-requests/:reqId/approve','/organizations/:id/join-requests/:reqId/reject','/organizations/:id/discovery-settings','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7023-8004-000000000063'::UUID, 'gr.org.member', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c001-7003-8002-000000000023'::UUID, '{"role_info":{"name":"gr.org.member","display_name":"Member","role_type":"default","priority":30,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET","PUT"],"description":"Own profile"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET"],"description":"View org detail"},{"route":"/organizations/:id/switch","methods":["POST"],"description":"Switch active org"},{"route":"/me/join-requests","methods":["GET"],"description":"Own join requests"},{"route":"/join-requests/:id","methods":["DELETE"],"description":"Cancel own join request"},{"route":"/me/notifications","methods":["GET","PUT"],"description":"Notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/organizations/:id/switch','/me/join-requests','/join-requests/:id','/me/notifications']), + ('0195948d-c003-7024-8004-000000000064'::UUID, 'gr.org.observer', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c001-7004-8002-000000000024'::UUID, '{"role_info":{"name":"gr.org.observer","display_name":"Observer","role_type":"default","priority":10,"is_system":true},"permissions":[{"route":"/auth/login","methods":["POST"],"description":"Login"},{"route":"/auth/logout","methods":["PUT"],"description":"Logout"},{"route":"/auth/refresh","methods":["PUT"],"description":"Refresh token"},{"route":"/users/me","methods":["GET"],"description":"View own profile"},{"route":"/organizations/search","methods":["GET"],"description":"Search orgs"},{"route":"/organizations/:id","methods":["GET"],"description":"View org detail"},{"route":"/me/notifications","methods":["GET"],"description":"View notifications"}]}'::jsonb, ARRAY['/auth/login','/auth/logout','/auth/refresh','/users/me','/organizations/search','/organizations/:id','/me/notifications']), + ('0195948d-c003-7031-8004-000000000071'::UUID, 'gr.system.root', 'dae760ab-0a7f-4cbd-8603-def85ad8e430'::UUID, '0195948d-c002-7001-8003-000000000031'::UUID, '{"role_info":{"name":"gr.system.root","display_name":"Platform Root","role_type":"default","priority":999,"is_system":true},"permissions":[{"route":"*","methods":["GET","POST","PUT","DELETE","PATCH"],"description":"Unrestricted platform access — internal use only"}]}'::jsonb, ARRAY['*']) +ON CONFLICT (id) DO NOTHING; diff --git a/internal/controllers/orgControllers/org.go b/internal/controllers/orgControllers/org.go index 3e15729..293fecc 100644 --- a/internal/controllers/orgControllers/org.go +++ b/internal/controllers/orgControllers/org.go @@ -20,7 +20,7 @@ func errResp(ctx *fiber.Ctx, err error) error { func parseOrgId(ctx *fiber.Ctx) (uuid.UUID, error) { orgId, err := uuid.Parse(ctx.Params("id")) if err != nil { - ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ + _ = ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ // #nosec G104 -- fiber handles response errors Code: fiber.StatusBadRequest, Message: "invalid organization id", }) diff --git a/internal/controllers/projectControllers/project.go b/internal/controllers/projectControllers/project.go index 8276bb6..1519a91 100644 --- a/internal/controllers/projectControllers/project.go +++ b/internal/controllers/projectControllers/project.go @@ -22,7 +22,7 @@ func projectErrResp(ctx *fiber.Ctx, err error) error { func parseProjectId(ctx *fiber.Ctx) (uuid.UUID, error) { id, err := uuid.Parse(ctx.Params("id")) if err != nil { - ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ + _ = ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ // #nosec G104 -- fiber handles response errors Code: fiber.StatusBadRequest, Message: "invalid project id", }) @@ -33,7 +33,7 @@ func parseProjectId(ctx *fiber.Ctx) (uuid.UUID, error) { func parseOrgIdParam(ctx *fiber.Ctx) (uuid.UUID, error) { id, err := uuid.Parse(ctx.Params("orgId")) if err != nil { - ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ + _ = ctx.Status(fiber.StatusBadRequest).JSON(dbmodels.ServiceResponse{ // #nosec G104 -- fiber handles response errors Code: fiber.StatusBadRequest, Message: "invalid organization id", }) diff --git a/internal/middlewares/auth_chain.go b/internal/middlewares/auth_chain.go index bb1ce25..588fd00 100644 --- a/internal/middlewares/auth_chain.go +++ b/internal/middlewares/auth_chain.go @@ -66,7 +66,7 @@ func ApplicationKeyMiddleware() fiber.Handler { } // Cache the tenantID for this application key - cache.Set(cacheKey, tenantID, 1*time.Hour) + _ = cache.Set(cacheKey, tenantID, 1*time.Hour) // #nosec G104 -- best-effort cache log.Printf("✅ Cached application key: %s -> tenant: %s", key, tenantID) // Store tenant info for downstream middleware/handlers diff --git a/internal/middlewares/middlewares.go b/internal/middlewares/middlewares.go index 83e3bc2..6e059f6 100644 --- a/internal/middlewares/middlewares.go +++ b/internal/middlewares/middlewares.go @@ -85,7 +85,7 @@ func TenantMiddleWare() fiber.Handler { // Token is valid - cache it and continue log.Printf("✅ Token verified successfully for tenant: %s", tenant_id) - cache.Set(cacheKey, tenant_id, 24*time.Hour) + _ = cache.Set(cacheKey, tenant_id, 24*time.Hour) // #nosec G104 -- best-effort cache c.Locals("token", tokenStr) c.Locals("tenant_id", tenant_id) @@ -128,7 +128,7 @@ func VerifyRoleRouteMapping(roleId string, route string, method string) (bool, e hasAccess = flag - cache.Set(cacheKey, hasAccess, 30*time.Minute) + _ = cache.Set(cacheKey, hasAccess, 30*time.Minute) // #nosec G104 -- best-effort cache fmt.Println("the flag: ", flag) diff --git a/internal/middlewares/verify.go b/internal/middlewares/verify.go index ab77ee2..49fff54 100644 --- a/internal/middlewares/verify.go +++ b/internal/middlewares/verify.go @@ -37,7 +37,7 @@ func VerifyJWT(tokenStr string) (jwt.MapClaims, error) { if err != nil { // Token is invalid/expired - blacklist it for 1 hour - cache.Set("blacklist:"+tokenStr, "expired", 0) + _ = cache.Set("blacklist:"+tokenStr, "expired", 0) // #nosec G104 -- best-effort cache return nil, fmt.Errorf("error while verifying token: %w", err) } @@ -47,13 +47,13 @@ func VerifyJWT(tokenStr string) (jwt.MapClaims, error) { expiresAt := time.Unix(int64(exp), 0) ttl := time.Until(expiresAt) if ttl > 0 { - cache.Set("token:"+tokenStr, claims, ttl) + _ = cache.Set("token:"+tokenStr, claims, ttl) // #nosec G104 -- best-effort cache } } return claims, nil } // Token is invalid - blacklist it - cache.Set("blacklist:"+tokenStr, "invalid", 1*time.Hour) + _ = cache.Set("blacklist:"+tokenStr, "invalid", 1*time.Hour) // #nosec G104 -- best-effort cache return nil, fmt.Errorf("invalid token or claims") } diff --git a/internal/repo/login.go b/internal/repo/login.go index f3bf15a..641e0d3 100644 --- a/internal/repo/login.go +++ b/internal/repo/login.go @@ -110,6 +110,6 @@ func (l *LoginRepository) Logout(userId uuid.UUID) error { if update.Error != nil { return update.Error } - cache.Set("blacklist:"+loginDetails.JWTToken, "expired", 0) + _ = cache.Set("blacklist:"+loginDetails.JWTToken, "expired", 0) // #nosec G104 -- best-effort cache return nil } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 7b64a17..e452817 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -5,8 +5,10 @@ import ( "crypto/rand" "encoding/base64" "fmt" - mathrand "math/rand" + "math/big" "os" + "path/filepath" + "regexp" "time" "github.com/golang-jwt/jwt/v4" @@ -108,7 +110,11 @@ func ConvertTime(input string) time.Time { func GenerateRandomString(length int) string { b := make([]rune, length) for i := range b { - b[i] = charset[mathrand.Intn(len(charset))] + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + panic("crypto/rand failed: " + err.Error()) + } + b[i] = charset[n.Int64()] } return string(b) } @@ -116,7 +122,11 @@ func GenerateRandomString(length int) string { func GenerateOTP() string { b := make([]rune, 6) for i := range b { - b[i] = otpcharset[mathrand.Intn(len(otpcharset))] + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(otpcharset)))) + if err != nil { + panic("crypto/rand failed: " + err.Error()) + } + b[i] = otpcharset[n.Int64()] } return string(b) } @@ -131,8 +141,16 @@ func GeneratePassword(password string, p *Argon2Params, salt string) (string, er return hashBase64, nil } +// safeRoleName only allows alphanumeric, dots, underscores, and hyphens — +// prevents path traversal when roleName originates from DB data. +var safeRoleName = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + func ReadPermissionFile(roleName string) (string, error) { - file, err := os.Open("./permissions/" + roleName + ".json") + if !safeRoleName.MatchString(roleName) { + return "", fmt.Errorf("invalid role name: %q", roleName) + } + path := filepath.Join("./permissions", roleName+".json") + file, err := os.Open(path) // #nosec G304 -- path validated by safeRoleName above if err != nil { return "", err } diff --git a/main.go b/main.go index cab0b45..d0a91c5 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "log" "os" + "strings" "github.com/vviveksharma/auth/config" ) @@ -11,7 +12,12 @@ var ServerMode string // Set via ldflags during build func main() { // Check environment variable first, then build-time flag - mode := os.Getenv("SERVER_MODE") + mode := strings.Map(func(r rune) rune { + if r == '\n' || r == '\r' { + return -1 // strip newlines to prevent log injection (G706) + } + return r + }, os.Getenv("SERVER_MODE")) if mode == "" { mode = ServerMode // From ldflags } diff --git a/queue/queue.go b/queue/queue.go index 5a41bc1..d860233 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -172,10 +172,10 @@ func (q *QueueService) ConsumeMessages(conn *amqp.Connection, queue amqp.Queue) // Acknowledge or reject message if processErr != nil { log.Printf("❌ Error processing message: %v", processErr) - msg.Nack(false, true) + _ = msg.Nack(false, true) // #nosec G104 -- redelivery handled by AMQP broker } else { log.Printf("✅ Message processed successfully") - msg.Ack(false) + _ = msg.Ack(false) // #nosec G104 -- redelivery handled by AMQP broker } }