Compare commits

...

9 Commits
main ... dev

Author SHA1 Message Date
Vitaliy Pavlov
2c4eaf6809 stage WIP 2024-08-23 20:57:09 +07:00
Vitaliy Pavlov
327208b333 get tasks status example 2024-08-22 23:14:58 +07:00
Vitaliy Pavlov
9d95c05457 update message examples 2024-08-22 22:50:41 +07:00
Vitaliy Pavlov
1f9a95897e update types and add plugins submodule 2024-07-31 20:55:46 +07:00
Vitaliy Pavlov
61fe7dbea5 tasks and plugins (WIP) 2024-07-31 20:14:46 +07:00
Vitaliy Pavlov
5e3ebdc825 completed reset password feature 2024-07-29 10:34:54 +07:00
Vitaliy Pavlov
50b19bf5a3 completed many items features 2024-07-29 07:21:50 +07:00
Vitaliy Pavlov
b2b2562e09 completed user create and edit features 2024-07-27 04:02:08 +07:00
Vitaliy Pavlov
13154a1b4e completed add group feature 2024-07-22 23:20:31 +07:00
44 changed files with 1588 additions and 100 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
.env
tmp
tmp
.DS_Store

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "plugins/dictionary"]
path = plugins/dictionary
url = ../../plugins-dictionary

32
amqp/amqp.go Normal file
View File

@ -0,0 +1,32 @@
package amqp
import (
"log"
"os"
"sync"
"system-trace/core/environment"
"system-trace/core/utils"
"github.com/wagslane/go-rabbitmq"
)
var Broker *rabbitmq.Conn
func InitConn() *rabbitmq.Conn {
var conn *rabbitmq.Conn
conn, err := rabbitmq.NewConn(
os.Getenv("AMQP_URL"),
rabbitmq.WithConnectionOptionsLogging,
rabbitmq.WithConnectionOptionsReconnectInterval(environment.ReconnectionInterval),
)
if err != nil {
log.Println("[AMQP]", err)
wg := sync.WaitGroup{}
wg.Add(1)
to := environment.ReconnectionInterval
utils.WaitTimeout(&wg, to)
return InitConn()
}
return conn
}

43
amqp/types/agent.go Normal file
View File

@ -0,0 +1,43 @@
package types
const (
AGENT_HELLO uint8 = iota // 0
AGENT_ERROR // 1
AGENT_STATUS // 2
AGENT_PING // 3
)
const (
TASK_STATUS_PREPARING uint8 = iota // 0
TASK_STATUS_RUNNING // 1
TASK_STATUS_STOPPED // 2
TASK_STATUS_FINISHED // 3
TASK_STATUS_ERROR // 4
)
type AgentMessage struct {
Message uint8 `json:"message"`
Data *agentMessageData `json:"data"`
}
type agentMessageData struct {
agentHelloData
agentErrorData
agentStatusData
}
type agentErrorData struct {
Error string `json:"error"`
}
type agentStatusData struct {
Status uint8 `json:"status"`
TaskID int64 `json:"taskId"`
Progress uint8 `json:"progress"` // 0-100, for TASK_STATUS_RUNNING
}
type agentHelloData struct {
Hostname string `json:"hostname"`
Version string `json:"version"`
Interfaces []string `json:"interfaces"`
}

36
amqp/types/controller.go Normal file
View File

@ -0,0 +1,36 @@
package types
import (
"system-trace/core/types/constructor"
)
const (
CONTROLLER_TASK_START uint8 = iota // 0
CONTROLLER_TASK_PAUSE // 1
CONTROLLER_TASK_STOP // 2
CONTROLLER_PONG // 3
CONTROLLER_GET_TASKS_STATUS // 4
)
type ControllerMessage struct {
Message uint8 `json:"message" validate:"required,min=0,max=255"`
Data *controllerMessageData `json:"data" validate:"required"`
}
type controllerMessageData struct {
taskStartData
taskStatusData
getTasksStatus
}
type taskStatusData struct {
TaskID int64 `json:"taskId" validate:"required"`
}
type getTasksStatus struct {
Tasks []int64 `json:"tasks" validate:"required"`
}
type taskStartData struct {
Data *constructor.TaskData `json:"data" validate:"required"`
}

8
amqp/types/queues_map.go Normal file
View File

@ -0,0 +1,8 @@
package types
import "github.com/wagslane/go-rabbitmq"
type QueuesPair struct {
Publisher *rabbitmq.Publisher
Consumer *rabbitmq.Consumer
}

View File

@ -7,7 +7,7 @@ import (
"github.com/uptrace/bun"
)
type Server struct {
type Agent struct {
ID int64 `bun:",pk,autoincrement"`
Hostname string `bun:",notnull" json:"hostname"`
IP string `bun:",notnull"`
@ -17,9 +17,9 @@ type Server struct {
UpdatedAt time.Time `json:"updatedAt"`
}
var _ bun.BeforeAppendModelHook = (*Server)(nil)
var _ bun.BeforeAppendModelHook = (*Agent)(nil)
func (s *Server) BeforeAppendModel(ctx context.Context, query bun.Query) error {
func (s *Agent) BeforeAppendModel(ctx context.Context, query bun.Query) error {
switch query.(type) {
case *bun.InsertQuery:
s.LastOnline = time.Now()

View File

@ -17,8 +17,9 @@ import (
)
var PG *bun.DB
var ctx = context.Background()
func Connect() {
func Connect() *bun.DB {
pgconn := pgdriver.NewConnector(
pgdriver.WithNetwork("tcp"),
pgdriver.WithAddr(fmt.Sprintf("%s:%s", os.Getenv("DB_HOST"), os.Getenv("DB_PORT"))),
@ -50,7 +51,7 @@ func Connect() {
}
}
PG = db
return db
}
func createSchema(db *bun.DB) error {
@ -58,8 +59,8 @@ func createSchema(db *bun.DB) error {
(*Group)(nil),
(*GroupPermission)(nil),
(*User)(nil),
(*Session)(nil),
(*Server)(nil),
(*Task)(nil),
(*Agent)(nil),
(*AuthToken)(nil),
}
ctx := context.Background()

View File

@ -13,7 +13,7 @@ import (
type Group struct {
ID int32 `bun:",pk,autoincrement"`
IssuerID int32 `bun:",notnull" json:"issuerId"`
IssuerID int32 `bun:",notnull" json:"issuerID"`
Issuer *User `bun:"rel:belongs-to,join:issuer_id=id" json:"issuer"`
Name string `bun:",notnull,unique" json:"name"`
Users []*User `bun:"rel:has-many,join:id=group_id,array" json:"users"`
@ -33,8 +33,7 @@ func (g *Group) BeforeAppendModel(ctx context.Context, query bun.Query) error {
return nil
}
func FindGroups(s string, p *types.Pagination, ob *types.OrderBy) (groups *[]Group, cursor *types.Cursor, err error) {
ctx := context.Background()
func FindGroups(s *types.Search, p *types.Pagination, ob *types.OrderBy) (groups *[]Group, cursor *types.Cursor, err error) {
groups = new([]Group)
q := PG.NewSelect().
Model(groups).
@ -45,15 +44,15 @@ func FindGroups(s string, p *types.Pagination, ob *types.OrderBy) (groups *[]Gro
Limit(p.Count).
Order(fmt.Sprintf("%s %s", ob.Key, ob.Order))
if len(s) > 0 {
if len(s.Input) > 0 {
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
if parsers.IsInt(s) {
return q.Where(`"group"."id" = ?`, s)
if parsers.IsInt(s.Input) {
return q.Where(`"group"."id" = ?`, s.Input)
} else {
return q.
Where(`"group"."name" LIKE ?`, "%"+s+"%").
WhereOr(`"issuer"."email" LIKE ?`, "%"+s+"%").
WhereOr(`"issuer"."real_name" LIKE ?`, "%"+s+"%")
Where(`"group"."name" LIKE ?`, "%"+s.Input+"%").
WhereOr(`"issuer"."email" LIKE ?`, "%"+s.Input+"%").
WhereOr(`"issuer"."real_name" LIKE ?`, "%"+s.Input+"%")
}
})
}
@ -69,7 +68,6 @@ func FindGroups(s string, p *types.Pagination, ob *types.OrderBy) (groups *[]Gro
}
func FindGroupByID(id int) (group *Group, err error) {
ctx := context.Background()
group = new(Group)
err = PG.NewSelect().
Model(group).
@ -80,17 +78,16 @@ func FindGroupByID(id int) (group *Group, err error) {
}
func InsertGroup(g *Group) error {
ctx := context.Background()
_, err := PG.NewInsert().
Model(g).
Returning("NULL").
Returning("id").
Exec(ctx)
return err
}
func UpdateGroup(g *Group, cols []string) error {
ctx := context.Background()
cols = append(cols, "updated_at")
_, err := PG.NewUpdate().
Model(g).
Column(cols...).
@ -101,7 +98,6 @@ func UpdateGroup(g *Group, cols []string) error {
}
func DeleteGroup(id int) error {
ctx := context.Background()
_, err := PG.NewDelete().
Model(new(Group)).
Where("id = ?", id).
@ -109,3 +105,12 @@ func DeleteGroup(id int) error {
return err
}
func DeleteManyGroups(ids []int) error {
_, err := PG.NewDelete().
Model(new(Group)).
Where("id IN (?)", bun.In(ids)).
Exec(ctx)
return err
}

View File

@ -4,3 +4,21 @@ type GroupPermission struct {
GroupID int32 `bun:",notnull" json:"-"`
Value int8 `bun:",notnull" json:"value"`
}
func InsertGroupPermissions(gp []*GroupPermission) error {
_, err := PG.NewInsert().
Model(&gp).
Returning("NULL").
Exec(ctx)
return err
}
func DeleteGroupPermissions(groupid int32) error {
_, err := PG.NewDelete().
Model(new(GroupPermission)).
Where("group_id = ?", groupid).
Exec(ctx)
return err
}

View File

@ -7,10 +7,12 @@ import (
"github.com/uptrace/bun"
)
type Session struct {
type Task struct {
ID int64 `bun:",pk,autoincrement"`
IssuerID int32 `bun:",notnull" json:"issuerId"`
IssuerID int32 `bun:",notnull" json:"issuerID"`
Issuer *User `bun:"rel:belongs-to,join:issuer_id=id" json:"issuer"`
Status int8 `bun:"default:0" json:"status"`
Error string `json:"error"`
Configuration map[string]interface{} `bun:"type:jsonb" json:"configuration"`
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
FinishedAt time.Time `json:"finishedAt"`
@ -18,9 +20,9 @@ type Session struct {
DeletedAt time.Time `bun:",soft_delete,nullzero" json:"deletedAt"`
}
var _ bun.BeforeAppendModelHook = (*Session)(nil)
var _ bun.BeforeAppendModelHook = (*Task)(nil)
func (s *Session) BeforeAppendModel(ctx context.Context, query bun.Query) error {
func (s *Task) BeforeAppendModel(ctx context.Context, query bun.Query) error {
switch query.(type) {
case *bun.UpdateQuery:
s.UpdatedAt = time.Now()

View File

@ -18,13 +18,13 @@ type User struct {
PasswordHash string `bun:",notnull,type:varchar(64)" json:"-"`
PasswordLength int8 `bun:",notnull" json:"passwordLength"`
RealName string `bun:",notnull" json:"realName"`
GroupID int32 `bun:",notnull" json:"groupId"`
GroupID int32 `bun:",notnull" json:"groupID"`
Group *Group `bun:"rel:belongs-to,join:group_id=id" json:"group"`
IsRequiredToSetPassword bool `bun:",notnull,default:true" json:"isRequiredToSetPassword"`
IsActive bool `bun:",notnull,default:true" json:"isActive"`
LastLogin time.Time `json:"lastLogin"`
LastLogin time.Time `bun:",nullzero" json:"lastLogin"`
CreatedAt time.Time `bun:",notnull,default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedAt time.Time `bun:",nullzero" json:"updatedAt"`
DeletedAt time.Time `bun:",soft_delete,nullzero" json:"deletedAt"`
}
@ -40,7 +40,6 @@ func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
func FindByEmailAndPassword(email, password string) (*User, error) {
passwordHash := utils.SHA256(password)
ctx := context.Background()
u := new(User)
err := PG.NewSelect().
Model(u).
@ -51,8 +50,7 @@ func FindByEmailAndPassword(email, password string) (*User, error) {
return u, err
}
func FindUsers(s string, p *types.Pagination, ob *types.OrderBy) (users *[]User, cursor *types.Cursor, err error) {
ctx := context.Background()
func FindUsers(s *types.Search, p *types.Pagination, ob *types.OrderBy) (users *[]User, cursor *types.Cursor, err error) {
users = new([]User)
q := PG.NewSelect().
Model(users).
@ -62,18 +60,22 @@ func FindUsers(s string, p *types.Pagination, ob *types.OrderBy) (users *[]User,
Limit(p.Count).
Order(fmt.Sprintf("%s %s", ob.Key, ob.Order))
if len(s) > 0 {
if len(s.Input) > 0 {
q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
if parsers.IsInt(s) {
return q.Where(`"user"."id" = ?`, s)
if parsers.IsInt(s.Input) {
return q.Where(`"user"."id" = ?`, s.Input)
} else {
return q.
Where(`"user"."email" LIKE ?`, "%"+s+"%").
WhereOr(`"user"."real_name" LIKE ?`, "%"+s+"%")
Where(`"user"."email" LIKE ?`, "%"+s.Input+"%").
WhereOr(`"user"."real_name" LIKE ?`, "%"+s.Input+"%")
}
})
}
if len(s.GroupID) > 0 && parsers.IsInt(s.GroupID) {
q.Where(`"user"."group_id" = ?`, s.GroupID)
}
count, err := q.ScanAndCount(ctx)
return users, &types.Cursor{
@ -85,7 +87,6 @@ func FindUsers(s string, p *types.Pagination, ob *types.OrderBy) (users *[]User,
}
func FindUserByID(id int) (user *User, err error) {
ctx := context.Background()
user = new(User)
err = PG.NewSelect().
Model(user).
@ -96,7 +97,6 @@ func FindUserByID(id int) (user *User, err error) {
}
func InsertUser(u *User) error {
ctx := context.Background()
_, err := PG.NewInsert().
Model(u).
Returning("NULL").
@ -106,7 +106,7 @@ func InsertUser(u *User) error {
}
func UpdateUser(u *User, cols []string) error {
ctx := context.Background()
cols = append(cols, "updated_at")
_, err := PG.NewUpdate().
Model(u).
Column(cols...).
@ -117,7 +117,6 @@ func UpdateUser(u *User, cols []string) error {
}
func DeleteUser(id int) error {
ctx := context.Background()
_, err := PG.NewDelete().
Model(new(User)).
Where("id = ?", id).
@ -125,3 +124,32 @@ func DeleteUser(id int) error {
return err
}
func DeleteManyUsers(ids []int) error {
_, err := PG.NewDelete().
Model(new(User)).
Where("id IN (?)", bun.In(ids)).
Exec(ctx)
return err
}
func BlockManyUsers(ids []int) error {
_, err := PG.NewUpdate().
Model(new(User)).
Set("is_active = ?", false).
Where("id IN (?)", bun.In(ids)).
Exec(ctx)
return err
}
func UnblockManyUsers(ids []int) error {
_, err := PG.NewUpdate().
Model(new(User)).
Set("is_active = ?", true).
Where("id IN (?)", bun.In(ids)).
Exec(ctx)
return err
}

View File

@ -108,6 +108,32 @@ const docTemplate = `{
}
}
},
"delete": {
"description": "Delete groups by ID",
"produces": [
"application/json"
],
"tags": [
"groups"
],
"summary": "Delete many groups",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "Update group with specified data",
"produces": [
@ -210,6 +236,26 @@ const docTemplate = `{
}
}
},
"/plugins": {
"get": {
"description": "Returns key-value map with plugins",
"produces": [
"application/json"
],
"summary": "Get list of available plugins",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/constructor.Plugin"
}
}
}
}
}
},
"/users": {
"get": {
"description": "Returns array of users and count",
@ -267,6 +313,35 @@ const docTemplate = `{
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.NewCredentials"
}
}
}
},
"delete": {
"description": "Delete users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
@ -354,9 +429,109 @@ const docTemplate = `{
}
}
}
},
"/users/block": {
"patch": {
"description": "Block users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Block many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/users/password/:id": {
"patch": {
"description": "Reset user password by user ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Reset user password",
"parameters": [
{
"minimum": 1,
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.NewCredentials"
}
}
}
}
},
"/users/unblock": {
"patch": {
"description": "Unblock users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Unblock many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"definitions": {
"constructor.Plugin": {
"type": "object",
"properties": {
"ID": {
"type": "integer"
},
"name": {
"type": "string"
},
"onlyUdp": {
"type": "boolean"
}
}
},
"database.Group": {
"type": "object",
"properties": {
@ -433,9 +608,6 @@ const docTemplate = `{
"lastLogin": {
"type": "string"
},
"passwordHash": {
"type": "string"
},
"passwordLength": {
"type": "integer"
},
@ -475,6 +647,9 @@ const docTemplate = `{
},
"totalPages": {
"type": "number"
},
"totalRows": {
"type": "number"
}
}
},
@ -486,6 +661,31 @@ const docTemplate = `{
},
"data": {}
}
},
"types.ManyIDs": {
"type": "object",
"required": [
"array"
],
"properties": {
"array": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"types.NewCredentials": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {

View File

@ -100,6 +100,32 @@
}
}
},
"delete": {
"description": "Delete groups by ID",
"produces": [
"application/json"
],
"tags": [
"groups"
],
"summary": "Delete many groups",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "Update group with specified data",
"produces": [
@ -202,6 +228,26 @@
}
}
},
"/plugins": {
"get": {
"description": "Returns key-value map with plugins",
"produces": [
"application/json"
],
"summary": "Get list of available plugins",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/constructor.Plugin"
}
}
}
}
}
},
"/users": {
"get": {
"description": "Returns array of users and count",
@ -259,6 +305,35 @@
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.NewCredentials"
}
}
}
},
"delete": {
"description": "Delete users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Delete many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
@ -346,9 +421,109 @@
}
}
}
},
"/users/block": {
"patch": {
"description": "Block users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Block many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/users/password/:id": {
"patch": {
"description": "Reset user password by user ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Reset user password",
"parameters": [
{
"minimum": 1,
"type": "integer",
"description": "User ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.NewCredentials"
}
}
}
}
},
"/users/unblock": {
"patch": {
"description": "Unblock users by ID",
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Unblock many users",
"parameters": [
{
"description": "Request body",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.ManyIDs"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"definitions": {
"constructor.Plugin": {
"type": "object",
"properties": {
"ID": {
"type": "integer"
},
"name": {
"type": "string"
},
"onlyUdp": {
"type": "boolean"
}
}
},
"database.Group": {
"type": "object",
"properties": {
@ -425,9 +600,6 @@
"lastLogin": {
"type": "string"
},
"passwordHash": {
"type": "string"
},
"passwordLength": {
"type": "integer"
},
@ -467,6 +639,9 @@
},
"totalPages": {
"type": "number"
},
"totalRows": {
"type": "number"
}
}
},
@ -478,6 +653,31 @@
},
"data": {}
}
},
"types.ManyIDs": {
"type": "object",
"required": [
"array"
],
"properties": {
"array": {
"type": "array",
"items": {
"type": "integer"
}
}
}
},
"types.NewCredentials": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {

View File

@ -1,5 +1,14 @@
basePath: /v1
definitions:
constructor.Plugin:
properties:
ID:
type: integer
name:
type: string
onlyUdp:
type: boolean
type: object
database.Group:
properties:
createdAt:
@ -50,8 +59,6 @@ definitions:
type: boolean
lastLogin:
type: string
passwordHash:
type: string
passwordLength:
type: integer
realName:
@ -79,6 +86,8 @@ definitions:
type: integer
totalPages:
type: number
totalRows:
type: number
type: object
types.JSONPagination:
properties:
@ -86,6 +95,22 @@ definitions:
$ref: '#/definitions/types.Cursor'
data: {}
type: object
types.ManyIDs:
properties:
array:
items:
type: integer
type: array
required:
- array
type: object
types.NewCredentials:
properties:
email:
type: string
password:
type: string
type: object
info:
contact:
name: https://peresvet.it
@ -111,6 +136,23 @@ paths:
tags:
- auth
/groups:
delete:
description: Delete groups by ID
parameters:
- description: Request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/types.ManyIDs'
produces:
- application/json
responses:
"200":
description: OK
summary: Delete many groups
tags:
- groups
get:
description: Returns array of groups and count
parameters:
@ -220,7 +262,37 @@ paths:
type: integer
type: object
summary: Get list of permissions
/plugins:
get:
description: Returns key-value map with plugins
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/constructor.Plugin'
type: array
summary: Get list of available plugins
/users:
delete:
description: Delete users by ID
parameters:
- description: Request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/types.ManyIDs'
produces:
- application/json
responses:
"200":
description: OK
summary: Delete many users
tags:
- users
get:
description: Returns array of users and count
parameters:
@ -277,6 +349,8 @@ paths:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.NewCredentials'
summary: Create user
tags:
- users
@ -317,6 +391,62 @@ paths:
summary: Get user by ID
tags:
- users
/users/block:
patch:
description: Block users by ID
parameters:
- description: Request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/types.ManyIDs'
produces:
- application/json
responses:
"200":
description: OK
summary: Block many users
tags:
- users
/users/password/:id:
patch:
description: Reset user password by user ID
parameters:
- description: User ID
in: path
minimum: 1
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/types.NewCredentials'
summary: Reset user password
tags:
- users
/users/unblock:
patch:
description: Unblock users by ID
parameters:
- description: Request body
in: body
name: request
required: true
schema:
$ref: '#/definitions/types.ManyIDs'
produces:
- application/json
responses:
"200":
description: OK
summary: Unblock many users
tags:
- users
securityDefinitions:
accessToken=...;refreshToken=...:
in: header

View File

@ -3,15 +3,25 @@ package environment
import (
"log"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
)
var ReconnectionInterval time.Duration
func Load() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
if recon, err := strconv.Atoi(os.Getenv("RECONNECT_INTERVAL")); err == nil {
ReconnectionInterval = time.Duration(recon) * time.Millisecond
} else {
log.Fatal("[AMQP]", err)
}
}
func IsDebug() bool {

3
go.mod
View File

@ -10,11 +10,13 @@ require (
github.com/gofiber/swagger v1.0.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joho/godotenv v1.5.1
github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb
github.com/swaggo/swag v1.16.3
github.com/uptrace/bun v1.2.1
github.com/uptrace/bun/dialect/pgdialect v1.2.1
github.com/uptrace/bun/driver/pgdriver v1.2.1
github.com/uptrace/bun/extra/bundebug v1.2.1
github.com/wagslane/go-rabbitmq v0.14.2
github.com/wasilibs/go-re2 v1.5.2
)
@ -45,6 +47,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect

7
go.sum
View File

@ -67,6 +67,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU=
github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -88,6 +90,8 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@ -130,11 +134,14 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/wagslane/go-rabbitmq v0.14.2 h1:3l75Unsy0b8sb3ILqJxMTXkQLUPI67BOuubV9YBjGLE=
github.com/wagslane/go-rabbitmq v0.14.2/go.mod h1:6sCLt2wZoxyC73G7u/yD6/RX/yYf+x5D8SQk8nsa4Lc=
github.com/wasilibs/go-re2 v1.5.2 h1:fDO2TJrRzRrv3jD0gzOvmZ2UM4Yt9YXOEdLrlNc/Ies=
github.com/wasilibs/go-re2 v1.5.2/go.mod h1:UqqxQ1O99boQUm1r61H/IYGiGQOS/P88K7hU5nLNkEg=
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=

13
main.go
View File

@ -1,8 +1,11 @@
package main
import (
"system-trace/core/amqp"
"system-trace/core/database"
"system-trace/core/environment"
"system-trace/core/plugins"
"system-trace/core/services/agents"
"system-trace/core/validators"
)
@ -16,7 +19,15 @@ import (
// @externalDocs.description OpenAPI
func main() {
environment.Load()
plugins.LoadPlugins()
validators.RegisterValidators()
database.Connect()
database.PG = database.Connect()
amqp.Broker = amqp.InitConn()
{
go agents.CreateMainQueueConsumer(amqp.Broker)
}
serveApp()
}

230
message_examples.json Normal file
View File

@ -0,0 +1,230 @@
Messages from Agent
- TO MAIN QUEUE:
AGENT_HELLO:
{
"message": AGENT_HELLO (0),
"data": {
"hostname": "<agent_hostname>",
"version": "<agent_version>",
"interfaces": ["ens0p0","ens0p1"]
}
}
AGENT_ERROR: // for errors that are not related to tasks
{
"message": AGENT_ERROR (1),
"data": {
"error": "<error>"
}
}
- TO SEPARATED QUEUE:
AGENT_STATUS:
{
"message": AGENT_STATUS (2),
"data": {
"status": TASK_STATUS_<STATUS>, // PREPARING (0), RUNNING (1), STOPPED (2), FINISHED (3), ERROR (4)
"taskId": <task_id>,
"progress": "Optional 0-100 for TASK_STATUS_RUNNIG",
"error": "Optional for TASK_STATUS_ERROR"
}
}
AGENT_PING:
{
"message": AGENT_PING (3),
}
Messages from Controller
- TO SEPARATED QUEUE:
CONTROLLER_PONG:
{
"message": CONTROLLER_PONG (3),
}
CONTROLLER_GET_TASKS_STATUS:
{
"message": CONTROLLER_GET_TASKS_STATUS (4),
"data": {
"tasks": [123, 456, 678909], // []int64
}
}
CONTROLLER_TASK_PAUSE:
{
"message": CONTROLLER_TASK_PAUSE (1),
"data": {
"taskId": <task_id>
}
}
CONTROLLER_TASK_STOP:
{
"message": CONTROLLER_TASK_STOP (2),
"data": {
"taskId": <task_id>
}
}
CONTROLLER_TASK_START:
{
"message": CONTROLLER_TASK_START (0),
"data": {
"taskId": <task_id>,
"data": {
"type": TYPE_VFIO (0), // TYPE_POSIX (1)
"mode": MODE_THROUGHPUT_BPS (0), // MODE_THROUGHPUT_PPS (1), MODE_TCP_CONNECTIONS (2), MODE_USERS (3),
"time": <duration_in_seconds>,
"sourceClient": [
"enp0s0": [
{
"IPs": ["127.0.0.0/24", "120.0.1.1/32"], // SRC-IPs mask cannot be duplicated into two or more entities within 1 network interface
"MACs": {
"mode": IP_HASH (1),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
},
{
"address": "01-B0-D0-63-C2-26",
}
],
},
"nextHops": {
"mode": ROUND_ROBIN (0),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
"weight": 47,
},
{
"address": "01-B0-D0-63-C2-26",
"weight": 53,
}
],
},
},
],
"enp0s1": [
{
"IPs": ["121.0.1.1/32"],
"MACs": {
"mode": IP_HASH (1),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
},
{
"address": "01-B0-D0-63-C2-26",
}
],
}, // every packet receives round-robin balancing mac
"nextHops": {
"mode": ROUND_ROBIN (0),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
"weight": 47,
},
{
"address": "01-B0-D0-63-C2-26",
"weight": 53,
}
],
},
},
{
"IPs": ["124.0.1.0/24", "127.0.1.0/16"],
"MACs": {
"mode": IP_HASH (1),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
},
{
"address": "01-B0-D0-63-C2-26",
}
],
}, // IPs hash, divide and bind to every IPv4/IPv6. MAC-адресов не может быть больше IPs-адресов
"nextHops": {
"mode": ROUND_ROBIN (0),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
"weight": 47,
},
{
"address": "01-B0-D0-63-C2-26",
"weight": 53,
}
],
},
},
{
"IPs": ["129.0.1.1/32"],
"MACs": {
"mode": RANDOM (2), // random MAC every packet
},
"nextHops": {
"mode": ROUND_ROBIN (0),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
"weight": 47,
},
{
"address": "01-B0-D0-63-C2-26",
"weight": 53,
}
],
},
},
{
"IPs": ["128.0.1.0/24"],
"MACs": {
"mode": GENERATE (3), // generate mac for every IPv4/IPv6 and bind it
},
"nextHops": {
"mode": ROUND_ROBIN (0),
"addresses": [
{
"address": "00-B0-D0-63-C2-26",
"weight": 47,
},
{
"address": "01-B0-D0-63-C2-26",
"weight": 53,
}
],
},
}
],
],
"sourceReceiver": [
<same_as_source_client>
],
"plugins": [
{
"plugin": <plugin_id_from_dictionary>,
"weight": <0-100 value>
},
{
"plugin": <plugin_id_from_dictionary>,
"weight": <0-100 value>
}
],
"performance": {
"maxBps": <optional uint64 depends on selected mode>,
"maxPps": <optional uint32 depends on selected mode>,
"maxTcpConnections": <optional uint32 depends on selected mode>, // max bps or max pps limit required with max tcp connections
"maxUsers": <optional uint32 depends on selected mode>
},
"tweaks": [[0, 10], [10, 60], [20, 60], [30, 120], ...]
}
}
}

View File

@ -5,7 +5,7 @@ import (
"strings"
"system-trace/core/constants"
"system-trace/core/database"
"system-trace/core/modules/auth"
"system-trace/core/services/auth"
"system-trace/core/types"
"system-trace/core/utils"
@ -20,7 +20,7 @@ func ValidateSession(c *fiber.Ctx) error {
})
}
if !validatePair(c, p) {
return c.Status(http.StatusForbidden).JSON(fiber.Map{
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{
"error": constants.UNAUTHORIZED,
})
}
@ -37,7 +37,7 @@ func validatePair(c *fiber.Ctx, p *types.PairTokens) bool {
claims, err := utils.ValidateJWT(p.AccessToken)
if (err != nil && strings.Contains(err.Error(), "token is expired")) || claims["iss"] != constants.JWT_APP_ISS {
rclaims, rerr := utils.ValidateJWT(p.RefreshToken)
if rerr != nil || (rerr != nil && strings.Contains(rerr.Error(), "token is expired")) || rclaims["sub"] != p.AccessToken {
if (rerr != nil && strings.Contains(rerr.Error(), "token is expired")) || rclaims["sub"] != p.AccessToken {
return false
}
@ -55,8 +55,6 @@ func validatePair(c *fiber.Ctx, p *types.PairTokens) bool {
if err != nil {
return false
}
userID = pt.UserID
}
userID = claims["sub"].(int32)

View File

@ -1,6 +1,8 @@
package parsers
import "unicode"
import (
"unicode"
)
func IsInt(s string) bool {
for _, c := range s {

26
parsers/many_ids.go Normal file
View File

@ -0,0 +1,26 @@
package parsers
import (
"errors"
"system-trace/core/types"
"system-trace/core/validators"
"github.com/gofiber/fiber/v2"
)
func GetManyIDs(c *fiber.Ctx) (*types.ManyIDs, error) {
ids := new(types.ManyIDs)
if err := c.BodyParser(ids); err != nil {
return nil, err
}
if err := validators.Validate(c, ids); err != nil {
return nil, err
}
if len(ids.Array) <= 0 {
return nil, errors.New("empty array")
}
return ids, nil
}

View File

@ -7,7 +7,7 @@ import (
"github.com/gofiber/fiber/v2"
)
func ParseOrder(c *fiber.Ctx) (*types.OrderBy, error) {
func GetOrderBy(c *fiber.Ctx) (*types.OrderBy, error) {
ob := new(types.OrderBy)
if err := c.QueryParser(ob); err != nil {
return nil, err

View File

@ -7,7 +7,7 @@ import (
"github.com/gofiber/fiber/v2"
)
func ParsePagination(c *fiber.Ctx) (*types.Pagination, error) {
func GetPagination(c *fiber.Ctx) (*types.Pagination, error) {
p := new(types.Pagination)
if err := c.QueryParser(p); err != nil {
return nil, err

16
parsers/search.go Normal file
View File

@ -0,0 +1,16 @@
package parsers
import (
"system-trace/core/types"
"github.com/gofiber/fiber/v2"
)
func GetSearch(c *fiber.Ctx) (*types.Search, error) {
s := new(types.Search)
if err := c.QueryParser(s); err != nil {
return nil, err
}
return s, nil
}

1
plugins/dictionary Submodule

@ -0,0 +1 @@
Subproject commit 40fcb5dc67bae495bccff549f3373dbbac854909

34
plugins/plugins.go Normal file
View File

@ -0,0 +1,34 @@
package plugins
import (
"encoding/json"
"log"
"os"
"system-trace/core/types/constructor"
"github.com/gofiber/fiber/v2"
)
var plugins *[]constructor.Plugin
func LoadPlugins() {
dictpath := "plugins/dictionary/dict.json"
d, err := os.ReadFile(dictpath)
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(d, &plugins); err != nil {
log.Fatal(err)
}
}
// MARK: GetPlugins godoc
// @Summary Get list of available plugins
// @Description Returns array of plugins
// @Produce json
// @Success 200 {object} []constructor.Plugin
// @Router /plugins [get]
func GetPlugins(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(plugins)
}

View File

@ -2,6 +2,7 @@ package main
import (
_ "system-trace/core/docs"
"system-trace/core/plugins"
"system-trace/core/services"
"system-trace/core/services/auth"
"system-trace/core/services/groups"
@ -17,6 +18,7 @@ func initRouter(app *fiber.App) {
{
v1.Get("/permissions", services.GetPermissions)
v1.Get("/plugins", plugins.GetPlugins)
ag := v1.Group("/auth")
{
@ -29,6 +31,7 @@ func initRouter(app *fiber.App) {
gg.Get("/:id", groups.GetGroupByIDHandler)
gg.Post("", groups.CreateGroupHandler)
gg.Patch("", groups.UpdateGroupHandler)
gg.Delete("", groups.DeleteGroupsHandler)
gg.Delete("/:id", groups.DeleteGroupHandler)
}
@ -38,6 +41,10 @@ func initRouter(app *fiber.App) {
ug.Get("/:id", users.GetUserByIDHandler)
ug.Post("", users.CreateUserHandler)
ug.Patch("", users.UpdateUserHandler)
ug.Patch("/password/:id", users.ResetUserPasswordHandler)
ug.Patch("/block", users.BlockUsersHandler)
ug.Patch("/unblock", users.UnblockUsersHandler)
ug.Delete("", users.DeleteUsersHandler)
ug.Delete("/:id", users.DeleteUserHandler)
}
}

13
services/agents/agents.go Normal file
View File

@ -0,0 +1,13 @@
package agents
import "system-trace/core/amqp/types"
var queues = make(map[string]*types.QueuesPair)
func getQueuePair(hostname string) *types.QueuesPair {
if pair, ok := queues[hostname]; ok {
return pair
}
return nil
}

View File

@ -0,0 +1,94 @@
package agents
import (
"encoding/json"
"fmt"
"log"
agent "system-trace/core/amqp/types"
"github.com/wagslane/go-rabbitmq"
)
var mainQueueName string = "SYSTEM_TRACE_MAIN"
func CreateMainQueueConsumer(broker *rabbitmq.Conn) {
consumer, err := rabbitmq.NewConsumer(
broker,
mainQueueName,
rabbitmq.WithConsumerOptionsRoutingKey(""),
rabbitmq.WithConsumerOptionsExchangeName("controller"),
rabbitmq.WithConsumerOptionsExchangeDeclare,
)
if err != nil {
log.Fatal(err)
}
if err = consumer.Run(handleMessageFromMainQueue); err != nil {
log.Fatal(err)
}
}
func handleMessageFromMainQueue(message rabbitmq.Delivery) rabbitmq.Action {
am := new(agent.AgentMessage)
if err := json.Unmarshal(message.Body, &am); err != nil {
// TODO error log
return rabbitmq.NackDiscard
}
action := handleAgentMessage(am)
return action
}
func handleAgentMessage(message *agent.AgentMessage) rabbitmq.Action {
switch message.Message {
case agent.AGENT_HELLO:
{
if message.Data == nil {
fmt.Println("No data in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
if len(message.Data.Hostname) <= 0 {
fmt.Println("Hostname field are not presented in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
if len(message.Data.Interfaces) <= 0 {
fmt.Println("Interfaces field are not presented in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
if len(message.Data.Version) <= 0 {
fmt.Println("Version field are not presented in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
// TODO create or update agent from hello message
return rabbitmq.Ack
}
case agent.AGENT_ERROR:
{
if message.Data == nil {
fmt.Println("No data in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
if len(message.Data.Error) <= 0 {
fmt.Println("Error field are not presented in received message hello")
// TODO error log
return rabbitmq.NackDiscard
}
// TODO save log
return rabbitmq.Ack
}
// case agent.AGENT_STATUS:
// {
// }
// case agent.AGENT_PING:
// {
// }
}
return rabbitmq.NackDiscard
}

View File

@ -50,8 +50,7 @@ func ReqTokensHandler(c *fiber.Ctx) error {
}
if u != nil {
err = GeneratePairAndSetCookie(c, u.ID)
if err != nil {
if err = GeneratePairAndSetCookie(c, u.ID); err != nil {
return c.
Status(fiber.StatusBadRequest).
JSON(types.JSONError{
@ -59,8 +58,7 @@ func ReqTokensHandler(c *fiber.Ctx) error {
})
}
err = users.SetLoginTime(u)
if err != nil {
if err = users.SetLoginTime(u); err != nil {
return c.
Status(fiber.StatusBadRequest).
JSON(types.JSONError{
@ -92,6 +90,7 @@ func setCookie(c *fiber.Ctx, p *types.PairTokens) {
atc.Secure = true
atc.HTTPOnly = true
c.Cookie(atc)
// Refresh token
rtc := new(fiber.Cookie)
rtc.Name = "refreshToken"

View File

@ -15,21 +15,25 @@ import (
// @Produce json
// @Param count query int true "Count of rows" minimum(10) maximum(100)
// @Param offset query int true "Rows to skip" minumum(0)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200 {object} types.JSONPagination
// @Router /groups [get]
func GetGroupsHandler(c *fiber.Ctx) error {
// TODO permission validate
s := c.Query("search")
p, err := parsers.ParsePagination(c)
s, err := parsers.GetSearch(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
ob, err := parsers.ParseOrder(c)
p, err := parsers.GetPagination(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
ob, err := parsers.GetOrderBy(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
@ -55,7 +59,6 @@ func GetGroupsHandler(c *fiber.Ctx) error {
// @Tags groups
// @Produce json
// @Param id path int true "Group ID" minimum(1)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200 {object} database.Group
// @Router /groups/:id [get]
func GetGroupByIDHandler(c *fiber.Ctx) error {
@ -83,7 +86,6 @@ func GetGroupByIDHandler(c *fiber.Ctx) error {
// @Tags groups
// @Produce json
// @Param request body database.Group true "Request body"
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Router /groups [post]
func CreateGroupHandler(c *fiber.Ctx) error {
@ -95,8 +97,19 @@ func CreateGroupHandler(c *fiber.Ctx) error {
})
}
err := database.InsertGroup(g)
if err != nil {
// TODO replace with locals
g.IssuerID = 2 //c.Locals("userId").(int32)
if err := database.InsertGroup(g); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
// Insert new permissions
for _, v := range g.Permissions {
v.GroupID = g.ID
}
if err := database.InsertGroupPermissions(g.Permissions); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
@ -111,7 +124,6 @@ func CreateGroupHandler(c *fiber.Ctx) error {
// @Tags groups
// @Produce json
// @Param request body database.Group true "Request body"
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Router /groups [patch]
func UpdateGroupHandler(c *fiber.Ctx) error {
@ -123,8 +135,24 @@ func UpdateGroupHandler(c *fiber.Ctx) error {
})
}
err := database.UpdateGroup(g, []string{"*"})
if err != nil {
if err := database.UpdateGroup(g, []string{"name"}); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
// Remove exist permissions
if err := database.DeleteGroupPermissions(g.ID); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
// Insert new permissions
for _, v := range g.Permissions {
v.GroupID = g.ID
}
if err := database.InsertGroupPermissions(g.Permissions); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
@ -139,7 +167,6 @@ func UpdateGroupHandler(c *fiber.Ctx) error {
// @Tags groups
// @Produce json
// @Param id path int true "Group ID" minimum(1)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Router /groups/:id [delete]
func DeleteGroupHandler(c *fiber.Ctx) error {
@ -151,8 +178,33 @@ func DeleteGroupHandler(c *fiber.Ctx) error {
})
}
err = database.DeleteGroup(id)
if err != nil {
if err = database.DeleteGroup(id); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.SendStatus(fiber.StatusOK)
}
// MARK: DeleteGroups godoc
// @Summary Delete many groups
// @Description Delete groups by ID
// @Tags groups
// @Produce json
// @Param request body types.ManyIDs true "Request body"
// @Success 200
// @Router /groups [delete]
func DeleteGroupsHandler(c *fiber.Ctx) error {
// TODO permission validate
arr := new(types.ManyIDs)
if err := c.BodyParser(arr); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
if err := database.DeleteManyGroups(arr.Array); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})

View File

@ -10,7 +10,6 @@ import (
// @Summary Get list of permissions
// @Description Returns key-value map with permissions
// @Produce json
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200 {object} map[string]int8
// @Router /permissions [get]
func GetPermissions(c *fiber.Ctx) error {

25
services/users/service.go Normal file
View File

@ -0,0 +1,25 @@
package users
import (
"system-trace/core/database"
"system-trace/core/utils"
"time"
)
func SetLoginTime(u *database.User) error {
u.LastLogin = time.Now()
return database.UpdateUser(u, []string{"last_login"})
}
func resetPassword(u *database.User) (string, error) {
pass, err := utils.GeneratePassword()
if err != nil {
return "", err
}
hash := utils.SHA256(pass)
u.PasswordHash = hash
u.PasswordLength = int8(len(pass))
u.IsRequiredToSetPassword = true
return pass, nil
}

View File

@ -4,7 +4,6 @@ import (
"system-trace/core/database"
"system-trace/core/parsers"
"system-trace/core/types"
"time"
"github.com/gofiber/fiber/v2"
)
@ -16,21 +15,25 @@ import (
// @Produce json
// @Param count query int true "Count of rows" minimum(10) maximum(100)
// @Param offset query int true "Rows to skip" minumum(0)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200 {object} types.JSONPagination
// @Router /users [get]
func GetUsersHandler(c *fiber.Ctx) error {
// TODO permission validate
s := c.Query("search")
p, err := parsers.ParsePagination(c)
s, err := parsers.GetSearch(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
ob, err := parsers.ParseOrder(c)
p, err := parsers.GetPagination(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
ob, err := parsers.GetOrderBy(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
@ -56,7 +59,6 @@ func GetUsersHandler(c *fiber.Ctx) error {
// @Tags users
// @Produce json
// @Param id path int true "User ID" minimum(1)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200 {object} database.User
// @Router /users/:id [get]
func GetUserByIDHandler(c *fiber.Ctx) error {
@ -84,8 +86,7 @@ func GetUserByIDHandler(c *fiber.Ctx) error {
// @Tags users
// @Produce json
// @Param request body database.User true "Request body"
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Success 200 {object} types.NewCredentials
// @Router /users [post]
func CreateUserHandler(c *fiber.Ctx) error {
// TODO permission validate
@ -96,14 +97,23 @@ func CreateUserHandler(c *fiber.Ctx) error {
})
}
err := database.InsertUser(u)
pass, err := resetPassword(u)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.SendStatus(fiber.StatusOK)
if err := database.InsertUser(u); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(types.NewCredentials{
Email: u.Email,
Password: pass,
})
}
// MARK: UpdateUser godoc
@ -112,7 +122,6 @@ func CreateUserHandler(c *fiber.Ctx) error {
// @Tags users
// @Produce json
// @Param request body database.User true "Request body"
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Router /users [patch]
func UpdateUserHandler(c *fiber.Ctx) error {
@ -124,8 +133,7 @@ func UpdateUserHandler(c *fiber.Ctx) error {
})
}
err := database.UpdateUser(u, []string{"*"})
if err != nil {
if err := database.UpdateUser(u, []string{"email", "real_name", "group_id"}); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
@ -140,7 +148,6 @@ func UpdateUserHandler(c *fiber.Ctx) error {
// @Tags users
// @Produce json
// @Param id path int true "User ID" minimum(1)
// @Header 200 {string} Token "accessToken=...;refreshToken=..."
// @Success 200
// @Router /users/:id [delete]
func DeleteUserHandler(c *fiber.Ctx) error {
@ -152,8 +159,7 @@ func DeleteUserHandler(c *fiber.Ctx) error {
})
}
err = database.DeleteUser(id)
if err != nil {
if err = database.DeleteUser(id); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
@ -162,7 +168,123 @@ func DeleteUserHandler(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
func SetLoginTime(u *database.User) error {
u.LastLogin = time.Now()
return database.UpdateUser(u, []string{"last_login"})
// MARK: DeleteUsers godoc
// @Summary Delete many users
// @Description Delete users by ID
// @Tags users
// @Produce json
// @Param request body types.ManyIDs true "Request body"
// @Success 200
// @Router /users [delete]
func DeleteUsersHandler(c *fiber.Ctx) error {
// TODO permission validate
arr := new(types.ManyIDs)
if err := c.BodyParser(arr); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
if err := database.DeleteManyUsers(arr.Array); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.SendStatus(fiber.StatusOK)
}
// MARK: BlockUsers godoc
// @Summary Block many users
// @Description Block users by ID
// @Tags users
// @Produce json
// @Param request body types.ManyIDs true "Request body"
// @Success 200
// @Router /users/block [patch]
func BlockUsersHandler(c *fiber.Ctx) error {
// TODO permission validate
arr, err := parsers.GetManyIDs(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
if err := database.BlockManyUsers(arr.Array); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.SendStatus(fiber.StatusOK)
}
// MARK: UnblockUsers godoc
// @Summary Unblock many users
// @Description Unblock users by ID
// @Tags users
// @Produce json
// @Param request body types.ManyIDs true "Request body"
// @Success 200
// @Router /users/unblock [patch]
func UnblockUsersHandler(c *fiber.Ctx) error {
// TODO permission validate
arr, err := parsers.GetManyIDs(c)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
if err := database.UnblockManyUsers(arr.Array); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.SendStatus(fiber.StatusOK)
}
// MARK: ResetUserPassword godoc
// @Summary Reset user password
// @Description Reset user password by user ID
// @Tags users
// @Produce json
// @Param id path int true "User ID" minimum(1)
// @Success 200 {object} types.NewCredentials
// @Router /users/password/:id [patch]
func ResetUserPasswordHandler(c *fiber.Ctx) error {
// TODO permission validate
id, err := c.ParamsInt("id")
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
user, err := database.FindUserByID(id)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
pass, err := resetPassword(user)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
if err := database.UpdateUser(user, []string{"password_hash", "password_length", "is_required_to_set_password"}); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(types.JSONError{
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(types.NewCredentials{
Email: user.Email,
Password: pass,
})
}

View File

@ -0,0 +1,7 @@
package constructor
type Plugin struct {
ID uint32 `json:"ID"`
Name string `json:"name"`
OnlyUDP bool `json:"onlyUdp"`
}

65
types/constructor/task.go Normal file
View File

@ -0,0 +1,65 @@
package constructor
const (
MODE_THROUGHPUT_BPS uint8 = iota // 0
MODE_THROUGHPUT_PPS // 1
MODE_TCP_CONNECTIONS // 2
MODE_USERS // 3
)
const (
TYPE_VFIO uint8 = iota // 0
TYPE_POSIX // 1
)
const (
MAC_MODE_ROUND_ROBIN uint8 = iota // 0 // every packet receives round-robin balancing mac
MAC_MODE_IP_HASH // 1 // ip hash, divide and bind to every IPv4/IPv6. MAC-адресов не может быть больше IP-адресов
MAC_MODE_RANDOM // 2 // random MAC every packet
MAC_MODE_GENERATE // 3 // generate mac for every IPv4/IPv6 and bind it
)
type TaskData struct {
Type uint8 `json:"type" validate:"required,min=0,max=255"`
Mode uint8 `json:"mode" validate:"required,min=0,max=255"`
Time uint32 `json:"time" validate:"required,min=0,max=4294967295"` // in seconds
SourceClient map[string]sourceData `json:"sourceClient" validate:"required"`
SourceReceiver map[string]sourceData `json:"sourceReceiver" validate:"required"`
Plugins []pluginData `json:"plugins" validate:"required"`
Performance performanceData `json:"performance" validate:"required"`
Tweaks tweaksData `json:"tweaks" validate:"required"` // [[time_in_seconds, value]]: [[10, 6570]]
}
type performanceData struct { // Depends on selected mode
// MODE_THROUGHPUT_BPS + MODE_TCP_CONNECTIONS
MaxBPS uint64 `json:"maxBps" validate:"min=0,max=18446744073709551615"`
// MODE_THROUGHPUT_PPS + MODE_TCP_CONNECTIONS
MaxPPS uint32 `json:"maxPps" validate:"min=0,max=4294967295"`
// MODE_TCP_CONNECTIONS
MaxTCPConnections uint32 `json:"maxTcpConnections" validate:"min=0,max=4294967295"`
// MODE_USERS
MaxUsers uint32 `json:"maxUsers" validate:"min=0,max=4294967295"`
}
type tweaksData [][]uint8
type pluginData struct {
Plugin uint32 `json:"plugin" validate:"min=0,max=4294967295"`
Weight uint8 `json:"weight" validate:"min=0,max=100"` // 0-100
}
type sourceData struct {
IPs []string `json:"IPs" validate:"required"` // SRC-IP mask cannot be duplicated into two or more entities within 1 network interface
MACs macData `json:"MACs" validate:"required"`
NextHops macData `json:"nextHops" validate:"required"`
}
type macData struct {
Mode uint8 `json:"mode" validate:"required,min=0,max=255"`
Addresses []macAddressData `json:"addresses"`
}
type macAddressData struct {
Address string `json:"address" validate:"required"`
Weight uint8 `json:"weight" validate:"min=0,max=100"` // 0-100
}

5
types/many_ids.go Normal file
View File

@ -0,0 +1,5 @@
package types
type ManyIDs struct {
Array []int `json:"array" validate:"required"`
}

6
types/new_credentials.go Normal file
View File

@ -0,0 +1,6 @@
package types
type NewCredentials struct {
Email string `json:"email"`
Password string `json:"password"`
}

View File

@ -1,6 +1,6 @@
package types
type OrderBy struct {
Key string `validate:"required"`
Order string `validate:"required,endswith=sc"`
Key string `query:"key" validate:"required"`
Order string `query:"order" validate:"required,endswith=sc"`
}

6
types/search.go Normal file
View File

@ -0,0 +1,6 @@
package types
type Search struct {
Input string `query:"input"`
GroupID string `query:"group_id"`
}

View File

@ -0,0 +1,23 @@
package utils
import (
"math/rand"
"github.com/lucasjones/reggen"
)
func GeneratePassword() (string, error) {
patterns := []string{
`[A-Z]{6,}[\d]{2,}[a-z]{2,}[@$!%*?&]{2,}[A-Za-z]{2,}`,
`[A-Za-z]{2,}[a-z]{6,}[\d]{2,}[@$!%*?&]{4,}`,
`[\d]{2,}[A-Z]{6,}[@$!%*?&]{4,}[a-z]{2,}`,
`[@$!%*?&]{4,}[a-z]{4,}[A-Z]{4,}[\d]{2,}`,
`[A-Z]{2,}[@$!%*?&]{6,}[a-z]{4,}[\d]{2,}`,
`[A-Z]{4,}[\d]{6,}[@$!%*?&]{4,}[a-z]{2,}`,
}
i := rand.Intn(len(patterns))
pass, err := reggen.Generate(patterns[i], 14)
return pass, err
}

20
utils/timeout.go Normal file
View File

@ -0,0 +1,20 @@
package utils
import (
"sync"
"time"
)
func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
c := make(chan struct{})
go func() {
defer close(c)
wg.Wait()
}()
select {
case <-c:
return false // completed normally
case <-time.After(timeout):
return true // timed out
}
}