chore: add auth logic
ci-agent / build (push) Failing after 7m51s

This commit is contained in:
d3m0k1d
2026-04-04 01:09:40 +03:00
parent d917a9e465
commit 8ab7fbc6b2
9 changed files with 543 additions and 7 deletions
+44 -3
View File
@@ -1,4 +1,4 @@
package cmd
package main
import (
"context"
@@ -27,17 +27,37 @@ func main() {
}
cfg, err := config.ImportSettings(cfg_path)
if err != nil {
log.Fatalf("Err loading config")
log.Fatalf("Err loading config: %v", err)
}
db, err := storage.Open(cfg.Database.Token_db)
if err != nil {
log.Fatalf("Err opening database")
log.Fatalf("Err opening database: %v", err)
}
defer db.Close()
h := handlers.New(db)
agents := handlers.AgentsGroup{Handlers: h}
auth := handlers.AuthGroup{Handlers: h}
// Create admin user from config if not exists
if cfg.Admin.Admin_login != "" && cfg.Admin.Admin_password != "" {
if !h.Repo.ExistsByLogin(cfg.Admin.Admin_login) {
_, err := h.Repo.CreateToken(repository.TokenCreate{
Name: cfg.Admin.Admin_name,
LastName: cfg.Admin.Admin_last_name,
Login: cfg.Admin.Admin_login,
Password: cfg.Admin.Admin_password,
PermissionView: true,
PermissionAdmin: true,
})
if err != nil {
log.Printf("Warning: failed to create admin user: %v", err)
} else {
log.Println("Admin user created from config")
}
}
}
router := gin.Default()
docs.SwaggerInfo.BasePath = "/api/v1"
@@ -49,12 +69,33 @@ func main() {
v1 := router.Group("/api/v1")
{
// Auth routes (public)
authGroup := v1.Group("/auth")
{
authGroup.POST("/login", auth.Login)
}
// Auth token management (requires auth)
authTokenGroup := v1.Group("/auth")
authTokenGroup.Use(auth.AuthMiddleware())
{
authTokenGroup.POST("/token", handlers.RequireAdmin(), auth.CreateToken)
authTokenGroup.GET("/validate", auth.ValidateToken)
authTokenGroup.GET("/tokens", handlers.RequireAdmin(), auth.ListTokens)
authTokenGroup.DELETE("/token", auth.DeleteMyToken)
authTokenGroup.DELETE("/tokens/:login", handlers.RequireAdmin(), auth.DeleteToken)
}
// Agents (requires manage_agent permission)
agentsGroup := v1.Group("/agents")
agentsGroup.Use(auth.AuthMiddleware(), handlers.RequireManageAgent())
{
agentsGroup.GET("", agents.List)
}
// Logs (requires view permission)
logsGroup := v1.Group("/logs")
logsGroup.Use(auth.AuthMiddleware(), handlers.RequireView())
{
if cfg.Database.Clickhouse_host != "" {
chConn, err := storage.OpenClickHouse(storage.ClickHouseConfig{
+182
View File
@@ -0,0 +1,182 @@
package handlers
import (
"errors"
"net/http"
"strings"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"github.com/gin-gonic/gin"
)
// AuthGroup handles authentication routes.
type AuthGroup struct {
*Handlers
}
// Login authenticates a user by login and password, returns a token.
// @Summary Login
// @Description Authenticate with login and password, returns a token and permissions
// @Tags auth
// @Accept json
// @Param request body repository.LoginRequest true "Login credentials"
// @Success 200 {object} repository.LoginResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/login [post]
func (ag *AuthGroup) Login(c *gin.Context) {
var req repository.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
resp, err := ag.Repo.Login(req.Login, req.Password)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to authenticate"})
return
}
c.JSON(http.StatusOK, resp)
}
// CreateToken creates a new user.
// @Summary Create user
// @Description Creates a new user with permissions
// @Tags auth
// @Accept json
// @Param request body repository.TokenCreate true "User data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/token [post]
func (ag *AuthGroup) CreateToken(c *gin.Context) {
var tc repository.TokenCreate
if err := c.ShouldBindJSON(&tc); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if _, err := ag.Repo.CreateToken(tc); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user created"})
}
// ValidateToken validates the current Bearer token and returns user info.
// @Summary Validate token
// @Description Check if the provided Bearer token is valid and return its permissions
// @Tags auth
// @Produce json
// @Success 200 {object} repository.Tokens
// @Failure 401 {object} map[string]string
// @Router /auth/validate [get]
func (ag *AuthGroup) ValidateToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token context"})
return
}
c.JSON(http.StatusOK, token)
}
// ListTokens returns all users.
// @Summary List users
// @Description Returns list of all users with their permissions
// @Tags auth
// @Produce json
// @Success 200 {array} repository.Tokens
// @Failure 500 {object} map[string]string
// @Router /auth/tokens [get]
func (ag *AuthGroup) ListTokens(c *gin.Context) {
tokens, err := ag.Repo.ListTokens()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list users"})
return
}
c.JSON(http.StatusOK, tokens)
}
// DeleteToken deletes a user by login from URL path.
// @Summary Delete user
// @Description Deletes a user by their login
// @Tags auth
// @Param login path string true "Login of the user to delete"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/tokens/:login [delete]
func (ag *AuthGroup) DeleteToken(c *gin.Context) {
login := c.Param("login")
if login == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "login required"})
return
}
if err := ag.Repo.DeleteTokenByLogin(login); err != nil {
if errors.Is(err, repository.ErrNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "user deleted"})
}
// DeleteMyToken deletes the current user's account.
// @Summary Delete my account
// @Description Deletes the current authenticated user
// @Tags auth
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/token [delete]
func (ag *AuthGroup) DeleteMyToken(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token context"})
return
}
if err := ag.Repo.DeleteToken(token.Token); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete account"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "account deleted"})
}
// getTokenFromHeader extracts the Bearer token from the Authorization header.
func getTokenFromHeader(c *gin.Context) string {
auth := c.GetHeader("Authorization")
if auth == "" {
return ""
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return ""
}
return parts[1]
}
+86
View File
@@ -0,0 +1,86 @@
package handlers
import (
"net/http"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"github.com/gin-gonic/gin"
)
// TokenContextKey is the context key for storing authenticated token info.
type TokenContextKey string
const tokenContextKey TokenContextKey = "token"
// AuthMiddleware validates that a Bearer token exists and is valid.
// It stores the token info in the context for later use.
// Returns 401 if token is missing or invalid.
func (ag *AuthGroup) AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := getTokenFromHeader(c)
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
c.Abort()
return
}
// Look up user by token value
tokens, err := ag.Repo.GetToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
c.Abort()
return
}
c.Set(string(tokenContextKey), tokens)
c.Next()
}
}
// RequirePermission is a generic permission checker.
func RequirePermission(check func(*repository.Tokens) bool) gin.HandlerFunc {
return func(c *gin.Context) {
tokenVal, exists := c.Get(string(tokenContextKey))
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "authentication required"})
c.Abort()
return
}
token, ok := tokenVal.(*repository.Tokens)
if !ok {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid token context"})
c.Abort()
return
}
if !check(token) {
c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
// RequireView requires permission_view.
func RequireView() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionView
})
}
// RequireManageAgent requires permission_manage_agent.
func RequireManageAgent() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionManage
})
}
// RequireAdmin requires permission_admin.
func RequireAdmin() gin.HandlerFunc {
return RequirePermission(func(t *repository.Tokens) bool {
return t.PermissionAdmin
})
}
+41
View File
@@ -0,0 +1,41 @@
package repository
// Tokens represents a user record with info and permissions.
type Tokens struct {
ID int64 `json:"id"`
Name string `json:"name"`
LastName string `json:"last_name"`
Login string `json:"login"`
Token string `json:"token"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
}
// TokenCreate is the request body for creating a new user.
type TokenCreate struct {
Name string `json:"name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
}
// LoginRequest is the request body for login.
type LoginRequest struct {
Login string `json:"login" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse is returned after successful login.
type LoginResponse struct {
Token string `json:"token"`
Name string `json:"name"`
LastName string `json:"last_name"`
Login string `json:"login"`
PermissionView bool `json:"permission_view"`
PermissionManage bool `json:"permission_manage_agent"`
PermissionAdmin bool `json:"permission_admin"`
}
+177 -1
View File
@@ -1,11 +1,187 @@
package repository
import "database/sql"
import (
"database/sql"
"errors"
"strconv"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/utils"
"golang.org/x/crypto/bcrypt"
)
// Repository wraps a SQLite database connection.
type Repository struct {
DB *sql.DB
}
// New creates a new Repository.
func New(db *sql.DB) *Repository {
return &Repository{DB: db}
}
var ErrNotFound = errors.New("not found")
// Init creates the tokens table if it does not exist.
func (r *Repository) Init() error {
_, err := r.DB.Exec(storage.CreateSqlite)
return err
}
// CreateToken inserts a new user record with hashed password and generated token.
func (r *Repository) CreateToken(tc TokenCreate) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(tc.Password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
token, err := utils.RandomToken()
if err != nil {
return "", err
}
result, err := r.DB.Exec(
`INSERT INTO tokens (name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
tc.Name, tc.LastName, tc.Login, string(hashed), token,
tc.PermissionView, tc.PermissionManage, tc.PermissionAdmin,
)
if err != nil {
return "", err
}
id, err := result.LastInsertId()
if err != nil {
return "", err
}
return strconv.FormatInt(id, 10), nil
}
// Login authenticates by login/password, generates a new token, and returns LoginResponse.
func (r *Repository) Login(login, password string) (*LoginResponse, error) {
var t Tokens
var hashedPassword string
err := r.DB.QueryRow(
`SELECT id, name, last_name, login, password, token, permission_view, permission_manage_agent, permission_admin
FROM tokens WHERE login = ?`,
login,
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &hashedPassword, &t.Token,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil {
return nil, ErrNotFound
}
// Generate new token on each login
newToken, err := utils.RandomToken()
if err != nil {
return nil, err
}
_, err = r.DB.Exec(`UPDATE tokens SET token = ? WHERE id = ?`, newToken, t.ID)
if err != nil {
return nil, err
}
return &LoginResponse{
Token: newToken,
Name: t.Name,
LastName: t.LastName,
Login: t.Login,
PermissionView: t.PermissionView,
PermissionManage: t.PermissionManage,
PermissionAdmin: t.PermissionAdmin,
}, nil
}
// GetTokenByToken retrieves a user record by token value.
func (r *Repository) GetToken(token string) (*Tokens, error) {
var t Tokens
err := r.DB.QueryRow(
`SELECT id, name, last_name, login, token, permission_view, permission_manage_agent, permission_admin
FROM tokens WHERE token = ?`,
token,
).Scan(&t.ID, &t.Name, &t.LastName, &t.Login, &t.Token,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return &t, nil
}
// ListTokens returns all users without password and token.
func (r *Repository) ListTokens() ([]Tokens, error) {
rows, err := r.DB.Query(
`SELECT id, name, last_name, login, permission_view, permission_manage_agent, permission_admin
FROM tokens`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var tokens []Tokens
for rows.Next() {
var t Tokens
if err := rows.Scan(&t.ID, &t.Name, &t.LastName, &t.Login,
&t.PermissionView, &t.PermissionManage, &t.PermissionAdmin); err != nil {
return nil, err
}
tokens = append(tokens, t)
}
return tokens, rows.Err()
}
// DeleteToken deletes a user by token value.
func (r *Repository) DeleteToken(token string) error {
result, err := r.DB.Exec(`DELETE FROM tokens WHERE token = ?`, token)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
// DeleteTokenByLogin deletes a user by login.
func (r *Repository) DeleteTokenByLogin(login string) error {
result, err := r.DB.Exec(`DELETE FROM tokens WHERE login = ?`, login)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrNotFound
}
return nil
}
// ExistsByLogin checks if a user with given login exists.
func (r *Repository) ExistsByLogin(login string) bool {
var count int
err := r.DB.QueryRow(`SELECT COUNT(*) FROM tokens WHERE login = ?`, login).Scan(&count)
if err != nil {
return false
}
return count > 0
}
+3 -2
View File
@@ -5,11 +5,12 @@ const CreateSqlite = `
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
last_name TEXT NOT NULL,
login TEXT NOT NULL,
login TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
permission_view BOOL NOT NULL,
permission_manage_agent BOOL NOT NULL,
permission_tokens BOOL NOT NULL
permission_admin BOOL NOT NULL
);
`
+6
View File
@@ -30,5 +30,11 @@ func Open(path string) (*sql.DB, error) {
if err != nil {
return nil, err
}
// Run migrations
if _, err := db.Exec(CreateSqlite); err != nil {
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
+1 -1
View File
@@ -1,4 +1,4 @@
package initial
package utils
import (
"crypto/rand"
+3
View File
@@ -0,0 +1,3 @@
services:
backend: