mirror of
https://github.com/veops/oneterm.git
synced 2025-09-27 03:36:02 +08:00
api server
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<a href=""><img src="https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c" alt="go>=1.18"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/veops/oneterm"><img src="https://goreportcard.com/badge/github.com/veops/oneterm" alt="API"></a>
|
||||
</p>
|
||||
oneterm: sample, lightweight, safe jump service
|
||||
oneterm simple, lightweight, safe jump service
|
||||
|
||||
---
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<a href=""><img src="https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c" alt="go>=1.18"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/veops/oneterm"><img src="https://goreportcard.com/badge/github.com/veops/oneterm" alt="API"></a>
|
||||
</p>
|
||||
oneterm: 简单、轻量、安全的跳板机服务
|
||||
oneterm 简单、轻量、安全的跳板机服务
|
||||
|
||||
---
|
||||
|
||||
|
109
backend/pkg/server/auth/acl/acl.go
Normal file
109
backend/pkg/server/auth/acl/acl.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
WRITE = "write"
|
||||
DELETE = "delete"
|
||||
READ = "read"
|
||||
GRANT = "grant"
|
||||
)
|
||||
|
||||
var (
|
||||
AllPermissions = []string{WRITE, DELETE, READ, GRANT}
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
Permissions []string `json:"permissions"`
|
||||
}
|
||||
|
||||
type ResourceResult struct {
|
||||
Groups []any `json:"groups"`
|
||||
Resources []*Resource `json:"resources"`
|
||||
}
|
||||
type Resource struct {
|
||||
AppID int `json:"app_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Deleted bool `json:"deleted"`
|
||||
DeletedAt string `json:"-"`
|
||||
ResourceId int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Permissions []string `json:"permissions"`
|
||||
ResourceTypeID int `json:"resource_type_id"`
|
||||
UID int `json:"uid"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Perm struct {
|
||||
Name string `json:"name"`
|
||||
Rid int `json:"rid"`
|
||||
}
|
||||
|
||||
type ResourcePermissionsRespItem struct {
|
||||
Perms []*Perm `json:"perms"`
|
||||
}
|
||||
|
||||
type Acl struct {
|
||||
Uid int `json:"uid"`
|
||||
UserName string `json:"userName"`
|
||||
Rid int `json:"rid"`
|
||||
RoleName string `json:"roleName"`
|
||||
ParentRoles []string `json:"parentRoles"`
|
||||
ChildRoles []string `json:"childRoles"`
|
||||
NickName string `json:"nickName"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Uid int `json:"uid"`
|
||||
Acl Acl `json:"acl"`
|
||||
}
|
||||
|
||||
func (s *Session) GetUid() int {
|
||||
return s.Uid
|
||||
}
|
||||
|
||||
func (s *Session) GetRid() int {
|
||||
return s.Acl.Rid
|
||||
}
|
||||
|
||||
func (s *Session) GetUserName() string {
|
||||
return s.Acl.UserName
|
||||
}
|
||||
|
||||
func (a *Acl) GetUserName(ctx *gin.Context) string {
|
||||
res, exist := ctx.Get("session")
|
||||
if exist {
|
||||
if v, ok := res.(*Session); ok {
|
||||
return v.GetUserName()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *Acl) GetUserInfo(ctx *gin.Context) (any, error) {
|
||||
res, exist := ctx.Get("session")
|
||||
if exist {
|
||||
if v, ok := res.(*Session); ok {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return res, fmt.Errorf("no session")
|
||||
}
|
||||
|
||||
type UserInfoResp struct {
|
||||
Result UserInfoRespResult `json:"result"`
|
||||
}
|
||||
|
||||
type UserInfoRespResult struct {
|
||||
Avatar string `json:"avatar"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Rid int `json:"rid"`
|
||||
Role Role `json:"role"`
|
||||
UID int `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
}
|
138
backend/pkg/server/auth/acl/itsdangrous.go
Normal file
138
backend/pkg/server/auth/acl/itsdangrous.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SigningAlgorithm provides interfaces to generate and verify signature
|
||||
type SigningAlgorithm interface {
|
||||
GetSignature(key, value string) []byte
|
||||
VerifySignature(key, value string, sig []byte) bool
|
||||
}
|
||||
|
||||
// HMACAlgorithm provides signature generation using HMACs.
|
||||
type HMACAlgorithm struct {
|
||||
DigestMethod func() hash.Hash
|
||||
}
|
||||
|
||||
// GetSignature returns the signature for the given key and value.
|
||||
func (a *HMACAlgorithm) GetSignature(key, value string) []byte {
|
||||
//a.DigestMethod().Reset()
|
||||
h := hmac.New(a.DigestMethod, []byte(key))
|
||||
h.Write([]byte(value))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// VerifySignature verifies the given signature matches the expected signature.
|
||||
func (a *HMACAlgorithm) VerifySignature(key, value string, sig []byte) bool {
|
||||
eq := subtle.ConstantTimeCompare(sig, []byte(a.GetSignature(key, value)))
|
||||
return eq == 1
|
||||
}
|
||||
|
||||
type Signature struct {
|
||||
SecretKey string
|
||||
Sep string
|
||||
Salt string
|
||||
KeyDerivation string
|
||||
DigestMethod func() hash.Hash
|
||||
Algorithm SigningAlgorithm
|
||||
}
|
||||
|
||||
// Unsign the given string.
|
||||
func (s *Signature) Unsign(signed string) (content []byte, err error) {
|
||||
if !strings.Contains(signed, s.Sep) {
|
||||
err = fmt.Errorf("no %s found in value", s.Sep)
|
||||
return
|
||||
}
|
||||
|
||||
li := strings.LastIndex(signed, s.Sep)
|
||||
value, sig := signed[:li], signed[li+len(s.Sep):]
|
||||
|
||||
if ok, _ := s.Verify(value, sig); ok {
|
||||
//c, err := base64Decode(strings.Split(strings.Trim(value, "."), ".")[0])
|
||||
var c []byte
|
||||
c, err = base64.RawURLEncoding.DecodeString(strings.Split(strings.Trim(value, "."), ".")[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var r io.ReadCloser
|
||||
r, err = zlib.NewReader(bytes.NewReader(c))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
err = fmt.Errorf("signature %s does not match", sig)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Signature) Verify(value, sig string) (bool, error) {
|
||||
key, err := s.DeriveKey()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
signed, err := base64.RawURLEncoding.DecodeString(sig)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return s.Algorithm.VerifySignature(key, value, signed), nil
|
||||
}
|
||||
|
||||
func (s *Signature) DeriveKey() (string, error) {
|
||||
var key string
|
||||
var err error
|
||||
|
||||
switch s.KeyDerivation {
|
||||
case "hmac":
|
||||
h := hmac.New(sha1.New, []byte(s.SecretKey))
|
||||
h.Write([]byte(s.Salt))
|
||||
|
||||
key = string(h.Sum(nil))
|
||||
case "none":
|
||||
key = s.SecretKey
|
||||
default:
|
||||
key, err = "", errors.New("unknown key derivation method")
|
||||
}
|
||||
|
||||
return key, err
|
||||
}
|
||||
|
||||
func NewSignature(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) *Signature {
|
||||
if salt == "" {
|
||||
salt = "itsdangerous.Signer"
|
||||
}
|
||||
if sep == "" {
|
||||
sep = "."
|
||||
}
|
||||
if derivation == "" {
|
||||
derivation = "hmac"
|
||||
}
|
||||
if digest == nil {
|
||||
digest = sha1.New
|
||||
}
|
||||
if algo == nil {
|
||||
algo = &HMACAlgorithm{DigestMethod: digest}
|
||||
}
|
||||
|
||||
return &Signature{
|
||||
SecretKey: secret,
|
||||
Salt: salt,
|
||||
Sep: sep,
|
||||
KeyDerivation: derivation,
|
||||
DigestMethod: digest,
|
||||
Algorithm: algo,
|
||||
}
|
||||
}
|
89
backend/pkg/server/auth/acl/login.go
Normal file
89
backend/pkg/server/auth/acl/login.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/remote"
|
||||
)
|
||||
|
||||
func LoginByPassword(ctx context.Context, username string, password string) (cookie string, err error) {
|
||||
url := fmt.Sprintf("%s/acl/login", conf.Cfg.Auth.Acl.Url)
|
||||
data := make(map[string]any)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"User-Agent": "oneterm",
|
||||
}).
|
||||
SetQueryParams(map[string]string{
|
||||
"channel": "ssh",
|
||||
}).
|
||||
SetResult(&data).
|
||||
SetBody(map[string]any{
|
||||
"username": username,
|
||||
// "password": fmt.Sprintf("%x", md5.Sum([]byte(password))),
|
||||
"password": password,
|
||||
}).
|
||||
Post(url)
|
||||
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cookie = resp.Header().Get("Set-Cookie")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func LoginByPublicKey(ctx context.Context, username string) (cookie string, err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/acl/users/info", conf.Cfg.Auth.Acl.Url)
|
||||
data := &UserInfoResp{}
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"User-Agent": "oneterm",
|
||||
}).
|
||||
SetQueryParams(map[string]string{
|
||||
"channel": "ssh",
|
||||
}).
|
||||
SetQueryParam("username", username).
|
||||
SetResult(&data).
|
||||
Get(url)
|
||||
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||
return
|
||||
}
|
||||
sess := &Session{
|
||||
Uid: data.Result.UID,
|
||||
Acl: Acl{
|
||||
Uid: data.Result.UID,
|
||||
UserName: data.Result.Username,
|
||||
Rid: data.Result.Rid,
|
||||
NickName: data.Result.Name,
|
||||
ParentRoles: data.Result.Role.Permissions,
|
||||
},
|
||||
}
|
||||
|
||||
bs, _ := json.Marshal(sess)
|
||||
|
||||
s := NewSignature(conf.Cfg.SecretKey, "cookie-session", "", "hmac", nil, nil)
|
||||
buf := &bytes.Buffer{}
|
||||
zw := zlib.NewWriter(buf)
|
||||
_, _ = zw.Write(bs)
|
||||
_ = zw.Close()
|
||||
value := "." + base64.RawURLEncoding.EncodeToString(buf.Bytes())
|
||||
dk, _ := s.DeriveKey()
|
||||
sign := s.Algorithm.GetSignature(dk, value)
|
||||
vs := value + "." + base64.RawURLEncoding.EncodeToString(sign)
|
||||
|
||||
cookie = "session=" + vs
|
||||
|
||||
return
|
||||
}
|
84
backend/pkg/server/auth/acl/perm.go
Normal file
84
backend/pkg/server/auth/acl/perm.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Package acl
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
)
|
||||
|
||||
func GetSessionFromCtx(ctx *gin.Context) (res *Session, err error) {
|
||||
res, ok := ctx.Value("session").(*Session)
|
||||
if !ok || res == nil {
|
||||
err = fmt.Errorf("empty session")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HasPerm(resourceId int, rid int, action string) bool {
|
||||
mapping, err := GetResourcePermissions(context.Background(), resourceId)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, v := range mapping {
|
||||
if lo.ContainsBy(v.Perms, func(p *Perm) bool { return p.Rid == rid && p.Name == action }) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsAdmin(session *Session) bool {
|
||||
for _, pr := range session.Acl.ParentRoles {
|
||||
if pr == "admin" || pr == "acl_admin" || pr == "oneterm_admin" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetResourceTypeName(resourceType string) string {
|
||||
names := conf.Cfg.Auth.Acl.ResourceNames
|
||||
for _, v := range names {
|
||||
if v.Key == resourceType {
|
||||
return v.Value
|
||||
}
|
||||
}
|
||||
return "NONE"
|
||||
}
|
||||
|
||||
func CreateGrantAcl(ctx context.Context, session *Session, resourceType string, resourceName string) (resourceId int, err error) {
|
||||
resource, err := AddResource(ctx,
|
||||
session.GetUid(),
|
||||
GetResourceTypeName(resourceType),
|
||||
resourceName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = GrantRoleResource(ctx, session.GetUid(), session.Acl.Rid, resource.ResourceId, AllPermissions); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resourceId = resource.ResourceId
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func CreateAcl(ctx context.Context, session *Session, resourceType string, resourceName string) (resourceId int, err error) {
|
||||
resource, err := AddResource(ctx,
|
||||
session.GetUid(),
|
||||
GetResourceTypeName(resourceType),
|
||||
resourceName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resourceId = resource.ResourceId
|
||||
|
||||
return
|
||||
}
|
82
backend/pkg/server/auth/acl/resource.go
Normal file
82
backend/pkg/server/auth/acl/resource.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/remote"
|
||||
)
|
||||
|
||||
func AddResource(ctx context.Context, uid int, resourceTypeId string, name string) (res *Resource, err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = &Resource{}
|
||||
url := fmt.Sprintf("%s/acl/resources", conf.Cfg.Auth.Acl.Url)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"X-User-Id": cast.ToString(uid)}).
|
||||
SetBody(map[string]any{
|
||||
"type_id": resourceTypeId,
|
||||
"name": name,
|
||||
"uid": uid,
|
||||
}).
|
||||
SetResult(&res).
|
||||
Post(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteResource(ctx context.Context, uid int, resourceId int) (err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%v/acl/resources/%v", conf.Cfg.Auth.Acl.Url, resourceId)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"X-User-Id": cast.ToString(uid)}).
|
||||
Delete(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateResource(ctx context.Context, uid int, resourceId int, updates map[string]string) (err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/acl/resources/%d", conf.Cfg.Auth.Acl.Url, resourceId)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"X-User-Id": cast.ToString(uid)}).
|
||||
SetFormData(updates).
|
||||
Put(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
||||
|
||||
func GetResourcePermissions(ctx context.Context, resourceId int) (res map[string]*ResourcePermissionsRespItem, err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = make(map[string]*ResourcePermissionsRespItem)
|
||||
url := fmt.Sprintf("%v/acl/resources/%v/permissions", conf.Cfg.Auth.Acl.Url, resourceId) //TODO conf
|
||||
resp, err := remote.RC.R().
|
||||
SetHeader("App-Access-Token", token).
|
||||
SetResult(&res).
|
||||
Get(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
130
backend/pkg/server/auth/acl/role.go
Normal file
130
backend/pkg/server/auth/acl/role.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/remote"
|
||||
)
|
||||
|
||||
func GetRoleResources(ctx context.Context, rid int, resourceTypeId string) (res []*Resource, err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := &ResourceResult{}
|
||||
url := fmt.Sprintf("%v/acl/roles/%v/resources", conf.Cfg.Auth.Acl.Url, rid)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeader("App-Access-Token", token).
|
||||
SetQueryParams(map[string]string{
|
||||
"app_id": conf.Cfg.Auth.Acl.AppId,
|
||||
"resource_type_id": resourceTypeId,
|
||||
}).
|
||||
SetResult(data).
|
||||
Get(url)
|
||||
|
||||
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = data.Resources
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func HasPermission(ctx context.Context, rid int, resourceName, resourceTypeName, permission string) (res bool, err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
url := fmt.Sprintf("%s/acl/roles/has_perm", conf.Cfg.Auth.Acl.Url)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeader("App-Access-Token", token).
|
||||
SetQueryParams(map[string]string{
|
||||
"rid": cast.ToString(rid),
|
||||
"resource_name": resourceName,
|
||||
"resource_type_name": resourceTypeName,
|
||||
"perm": permission,
|
||||
}).
|
||||
SetResult(&data).
|
||||
Get(url)
|
||||
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if v, ok := data["result"]; ok {
|
||||
res = v.(bool)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GrantRoleResource(ctx context.Context, uid int, roleId int, resourceId int, permissions []string) (err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/acl/roles/%d/resources/%d/grant", conf.Cfg.Auth.Acl.Url, roleId, resourceId)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"X-User-Id": cast.ToString(uid)}).
|
||||
SetBody(map[string]any{
|
||||
"perms": permissions,
|
||||
}).
|
||||
Post(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
||||
|
||||
func RevokeRoleResource(ctx context.Context, uid int, roleId int, resourceId int, permissions []string) (err error) {
|
||||
token, err := remote.GetAclToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/acl/roles/%d/resources/%d/revoke", conf.Cfg.Auth.Acl.Url, roleId, resourceId)
|
||||
resp, err := remote.RC.R().
|
||||
SetHeaders(map[string]string{
|
||||
"App-Access-Token": token,
|
||||
"X-User-Id": cast.ToString(uid)}).
|
||||
SetBody(map[string]any{
|
||||
"perms": permissions,
|
||||
}).
|
||||
Post(url)
|
||||
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
|
||||
return
|
||||
}
|
||||
|
||||
func BatchGrantRoleResource(ctx context.Context, uid int, roleIds []int, resourceId int, permissions []string) (err error) {
|
||||
eg := &errgroup.Group{}
|
||||
for _, rid := range roleIds {
|
||||
localRid := rid
|
||||
eg.Go(func() error {
|
||||
return GrantRoleResource(ctx, uid, localRid, resourceId, permissions)
|
||||
})
|
||||
}
|
||||
err = eg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func BatchRevokeRoleResource(ctx context.Context, uid int, roleIds []int, resourceId int, permissions []string) (err error) {
|
||||
eg := &errgroup.Group{}
|
||||
for _, rid := range roleIds {
|
||||
localRid := rid
|
||||
eg.Go(func() error {
|
||||
return RevokeRoleResource(ctx, uid, localRid, resourceId, permissions)
|
||||
})
|
||||
}
|
||||
err = eg.Wait()
|
||||
|
||||
return
|
||||
}
|
173
backend/pkg/server/cmdb/cmdb.go
Normal file
173
backend/pkg/server/cmdb/cmdb.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package cmdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/controller"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/remote"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
)
|
||||
|
||||
func Run() (err error) {
|
||||
currentUser := &acl.Session{
|
||||
Uid: conf.Cfg.Worker.Uid,
|
||||
Acl: acl.Acl{
|
||||
Rid: conf.Cfg.Worker.Rid,
|
||||
},
|
||||
}
|
||||
tk := time.NewTicker(time.Minute)
|
||||
last := make(map[int]time.Time)
|
||||
for {
|
||||
select {
|
||||
case <-tk.C:
|
||||
nodes, err := getNodes()
|
||||
if err != nil {
|
||||
logger.L.Error("get nodes faild", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
now := time.Now()
|
||||
for _, n := range nodes {
|
||||
d, _ := time.ParseDuration(fmt.Sprintf("%fh", n.Sync.Frequency))
|
||||
if last[n.Id].Add(d).After(now) {
|
||||
continue
|
||||
}
|
||||
if n.Sync.TypeId <= 0 {
|
||||
continue
|
||||
}
|
||||
cis, err := getCis(n.Sync.TypeId, n.Sync.Filters)
|
||||
if err != nil {
|
||||
logger.L.Error("get cmdb failed", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
for _, ci := range cis {
|
||||
a := &model.Asset{
|
||||
Ciid: cast.ToInt(ci["_id"]),
|
||||
ParentId: n.Id,
|
||||
UpdaterId: conf.Cfg.Worker.Uid,
|
||||
Ip: cast.ToString(ci[n.Sync.Mapping["ip"]]),
|
||||
Name: fmt.Sprintf("%s@%v", cast.ToString(ci[n.Sync.Mapping["name"]]), time.Now().Format(time.RFC3339)),
|
||||
Protocols: n.Protocols,
|
||||
Authorization: n.Authorization,
|
||||
AccessAuth: n.AccessAuth,
|
||||
}
|
||||
resourceIds := make([]int, 0)
|
||||
if err := mysql.DB.Model(a).Select("resource_id").Where("ci_id = ?", a.Ciid).Find(&resourceIds).Error; err != nil {
|
||||
logger.L.Error("insert ci failed", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if !errors.Is(mysql.DB.Model(a).Where("ci_id = ?", a.Ciid).Where("parent_id = ?", a.ParentId).First(map[string]any{}).Error, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
a.CreatorId = conf.Cfg.Worker.Uid
|
||||
|
||||
a.ResourceId, err = acl.CreateAcl(ctx, currentUser, acl.GetResourceTypeName(conf.RESOURCE_ASSET), a.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
if err = tx.Create(a).Error; err != nil {
|
||||
return
|
||||
}
|
||||
err = controller.HandleAuthorization(currentUser, tx, model.ACTION_CREATE, nil, a)
|
||||
return
|
||||
}); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Stop(err error) {
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
func getNodes() (res map[int]*model.Node, err error) {
|
||||
data := make([]*model.Node, 0)
|
||||
err = mysql.DB.
|
||||
Model(&model.Node{}).
|
||||
Where("enable = ?", 1).
|
||||
Find(&data).
|
||||
Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = lo.SliceToMap(data, func(d *model.Node) (int, *model.Node) { return d.Id, d })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getCis(typeId int, filters string) (res []map[string]any, err error) {
|
||||
url := fmt.Sprintf("%s/ci/s", conf.Cfg.Cmdb.Url)
|
||||
params := map[string]any{
|
||||
"q": fmt.Sprintf("_type:(%d),%s", typeId, filters),
|
||||
}
|
||||
params["_secret"] = buildAPIKey(url, params)
|
||||
params["_key"] = conf.Cfg.Worker.Key
|
||||
ps := make(map[string]string)
|
||||
for k, v := range params {
|
||||
ps[k] = cast.ToString(v)
|
||||
}
|
||||
|
||||
data := &GetCIResult{}
|
||||
resp, err := remote.RC.R().
|
||||
SetQueryParams(ps).
|
||||
SetResult(data).
|
||||
Get(url)
|
||||
|
||||
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = data.Result
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type GetCIResult struct {
|
||||
Counter map[string]int `json:"counter"`
|
||||
Facet map[string]any `json:"facet"`
|
||||
Numfound int `json:"numfound"`
|
||||
Page int `json:"page"`
|
||||
Result []map[string]any `json:"result"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
func buildAPIKey(u string, params map[string]any) string {
|
||||
pu, _ := url.Parse(u)
|
||||
keys := lo.Keys(params)
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
||||
vals := strings.Join(
|
||||
lo.Map(keys, func(k string, _ int) string {
|
||||
return lo.Ternary(strings.HasPrefix(k, "_"), "", cast.ToString(params[k]))
|
||||
}),
|
||||
"")
|
||||
sha := sha1.New()
|
||||
sha.Write([]byte(strings.Join([]string{pu.Path, conf.Cfg.Worker.Secret, vals}, "")))
|
||||
return hex.EncodeToString(sha.Sum(nil))
|
||||
}
|
171
backend/pkg/server/controller/account.go
Normal file
171
backend/pkg/server/controller/account.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
"github.com/veops/oneterm/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
accountPreHooks = []preHook[*model.Account]{
|
||||
func(ctx *gin.Context, data *model.Account) {
|
||||
if data.AccountType == model.AUTHMETHOD_PUBLICKEY {
|
||||
if data.Phrase == "" {
|
||||
_, err := ssh.ParsePrivateKey([]byte(data.Pk))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := ssh.ParsePrivateKeyWithPassphrase([]byte(data.Pk), []byte(data.Phrase))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data *model.Account) {
|
||||
data.Password = util.EncryptAES(data.Password)
|
||||
data.Pk = util.EncryptAES(data.Pk)
|
||||
data.Phrase = util.EncryptAES(data.Phrase)
|
||||
},
|
||||
}
|
||||
|
||||
accountPostHooks = []postHook[*model.Account]{
|
||||
func(ctx *gin.Context, data []*model.Account) {
|
||||
acs := make([]*model.AccountCount, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Select("account_id AS id, COUNT(*) as count").
|
||||
Group("account_id").
|
||||
Where("account_id IN ?", lo.Map(data, func(d *model.Account, _ int) int { return d.Id })).
|
||||
Find(&acs).
|
||||
Error; err != nil {
|
||||
return
|
||||
}
|
||||
m := lo.SliceToMap(acs, func(ac *model.AccountCount) (int, int64) { return ac.Id, ac.Count })
|
||||
for _, d := range data {
|
||||
d.AssetCount = m[d.Id]
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data []*model.Account) {
|
||||
for _, d := range data {
|
||||
d.Password = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Password), "")
|
||||
d.Pk = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Pk), "")
|
||||
d.Phrase = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Phrase), "")
|
||||
}
|
||||
},
|
||||
}
|
||||
accountDcs = []deleteCheck{
|
||||
func(ctx *gin.Context, id int) {
|
||||
assetName := ""
|
||||
err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("name").
|
||||
Where("id = (?)", mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("account_id = ?", id).Limit(1)).
|
||||
First(&assetName).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return
|
||||
}
|
||||
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
|
||||
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
|
||||
ctx.AbortWithError(code, err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreateAccount godoc
|
||||
//
|
||||
// @Tags account
|
||||
// @Param account body model.Account true "account"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /account [post]
|
||||
func (c *Controller) CreateAccount(ctx *gin.Context) {
|
||||
doCreate(ctx, true, &model.Account{}, conf.RESOURCE_ACCOUNT, accountPreHooks...)
|
||||
}
|
||||
|
||||
// DeleteAccount godoc
|
||||
//
|
||||
// @Tags account
|
||||
// @Param id path int true "account id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /account/:id [delete]
|
||||
func (c *Controller) DeleteAccount(ctx *gin.Context) {
|
||||
doDelete(ctx, true, &model.Account{}, accountDcs...)
|
||||
}
|
||||
|
||||
// UpdateAccount godoc
|
||||
//
|
||||
// @Tags account
|
||||
// @Param id path int true "account id"
|
||||
// @Param account body model.Account true "account"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /account/:id [put]
|
||||
func (c *Controller) UpdateAccount(ctx *gin.Context) {
|
||||
doUpdate(ctx, true, &model.Account{}, accountPreHooks...)
|
||||
}
|
||||
|
||||
// GetAccounts godoc
|
||||
//
|
||||
// @Tags account
|
||||
// @Param page_index query int true "page_index"
|
||||
// @Param page_size query int true "page_size"
|
||||
// @Param search query string false "name or account"
|
||||
// @Param id query int false "account id"
|
||||
// @Param ids query string false "account ids"
|
||||
// @Param name query string false "account name"
|
||||
// @Param info query bool false "is info mode"
|
||||
// @Param type query int false "account type"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Account}}
|
||||
// @Router /account [get]
|
||||
func (c *Controller) GetAccounts(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
db := mysql.DB.Model(&model.Account{})
|
||||
db = filterEqual(ctx, db, "id","type")
|
||||
db = filterLike(ctx, db, "name")
|
||||
db = filterSearch(ctx, db, "name", "account")
|
||||
if q, ok := ctx.GetQuery("ids"); ok {
|
||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||
}
|
||||
|
||||
if info && !acl.IsAdmin(currentUser) {
|
||||
//rs := make([]*acl.Resource, 0)
|
||||
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
ids := make([]int, 0)
|
||||
if err = mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })).
|
||||
Distinct().
|
||||
Pluck("account_id", &ids).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
db = db.Where("id IN ?", ids)
|
||||
}
|
||||
|
||||
db = db.Order("name")
|
||||
|
||||
doGet[*model.Account](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_ACCOUNT), accountPostHooks...)
|
||||
}
|
218
backend/pkg/server/controller/asset.go
Normal file
218
backend/pkg/server/controller/asset.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
assetPostHooks = []postHook[*model.Asset]{
|
||||
func(ctx *gin.Context, data []*model.Asset) {
|
||||
post := make([]*model.AssetNodeChain, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Node{}).
|
||||
Raw(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT id, name AS chain
|
||||
FROM node
|
||||
WHERE
|
||||
deleted_at = 0
|
||||
AND parent_id = 0
|
||||
UNION ALL
|
||||
SELECT
|
||||
t.id,
|
||||
CONCAT(cte.chain, '/', t.name)
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.id = t.parent_id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT *
|
||||
FROM cte
|
||||
`).
|
||||
Find(&post).
|
||||
Error; err != nil {
|
||||
logger.L.Error("asset posthookfailed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
m := lo.SliceToMap(post, func(p *model.AssetNodeChain) (int, string) { return p.NodeId, p.Chain })
|
||||
for _, d := range data {
|
||||
d.NodeChain = m[d.ParentId]
|
||||
}
|
||||
}, func(ctx *gin.Context, data []*model.Asset) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
if acl.IsAdmin(currentUser) {
|
||||
return
|
||||
}
|
||||
for _, a := range data {
|
||||
for k, v := range a.Authorization {
|
||||
if lo.Contains(v, currentUser.GetRid()) {
|
||||
continue
|
||||
}
|
||||
delete(a.Authorization, k)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreateAsset godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param asset body model.Asset true "asset"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /asset [post]
|
||||
func (c *Controller) CreateAsset(ctx *gin.Context) {
|
||||
doCreate(ctx, true, &model.Asset{}, conf.RESOURCE_ASSET)
|
||||
}
|
||||
|
||||
// DeleteAsset godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param id path int true "asset id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /asset/:id [delete]
|
||||
func (c *Controller) DeleteAsset(ctx *gin.Context) {
|
||||
doDelete(ctx, true, &model.Asset{})
|
||||
}
|
||||
|
||||
// UpdateAsset godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param id path int true "asset id"
|
||||
// @Param asset body model.Asset true "asset"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /asset/:id [put]
|
||||
func (c *Controller) UpdateAsset(ctx *gin.Context) {
|
||||
doUpdate(ctx, true, &model.Asset{})
|
||||
}
|
||||
|
||||
// GetAssets godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param page_index query int true "page_index"
|
||||
// @Param page_size query int true "page_size"
|
||||
// @Param search query string false "name or ip"
|
||||
// @Param id query int false "asset id"
|
||||
// @Param ids query string false "asset ids"
|
||||
// @Param parent_id query int false "asset's parent id"
|
||||
// @Param name query string false "asset name"
|
||||
// @Param ip query string false "asset ip"
|
||||
// @Param info query bool false "is info mode"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
|
||||
// @Router /asset [get]
|
||||
func (c *Controller) GetAssets(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
db := mysql.DB.Model(&model.Asset{})
|
||||
db = filterEqual(ctx, db, "id")
|
||||
db = filterLike(ctx, db, "name", "ip")
|
||||
db = filterSearch(ctx, db, "name", "ip")
|
||||
if q, ok := ctx.GetQuery("ids"); ok {
|
||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||
}
|
||||
if q, ok := ctx.GetQuery("parent_id"); ok {
|
||||
parentIds := make([]int, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Node{}).
|
||||
Raw(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT id
|
||||
FROM node
|
||||
WHERE deleted_at = 0
|
||||
AND parent_id = ?
|
||||
UNION ALL
|
||||
SELECT t.id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.id = t.parent_id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT id
|
||||
FROM cte;
|
||||
`, q).
|
||||
Find(&parentIds).
|
||||
Error; err != nil {
|
||||
logger.L.Error("parent id found failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
parentIds = append(parentIds, cast.ToInt(q))
|
||||
db = db.Where("parent_id IN ?", parentIds)
|
||||
}
|
||||
|
||||
if info && !acl.IsAdmin(currentUser) {
|
||||
//rs := make([]*acl.Resource, 0)
|
||||
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
ids := make([]int, 0)
|
||||
if err = mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })).
|
||||
Distinct().
|
||||
Pluck("asset_id", &ids).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
db = db.Where("id IN ?", ids)
|
||||
}
|
||||
|
||||
db = db.Order("name")
|
||||
|
||||
doGet[*model.Asset](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION), assetPostHooks...)
|
||||
}
|
||||
|
||||
// QueryByServer godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param page_index query int true "page index"
|
||||
// @Param page_size query int true "page size"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
|
||||
// @Router /asset/query_by_server [get]
|
||||
func (c *Controller) QueryByServer(ctx *gin.Context) {
|
||||
db := mysql.DB.Model(&model.Asset{})
|
||||
|
||||
doGet[*model.Asset](ctx, false, db, acl.GetResourceTypeName(conf.RESOURCE_ASSET), nil)
|
||||
}
|
||||
|
||||
// UpdateByServer godoc
|
||||
//
|
||||
// @Tags asset
|
||||
// @Param id path int true "asset id"
|
||||
// @Param req body map[int]map[string]any true "asset update request"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /asset/update_by_server [put]
|
||||
func (c *Controller) UpdateByServer(ctx *gin.Context) {
|
||||
updates := make(map[int]map[string]any)
|
||||
if err := ctx.BindJSON(&updates); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range updates {
|
||||
if err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Where("id = ?", k).
|
||||
Updates(v).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
153
backend/pkg/server/controller/authorization.go
Normal file
153
backend/pkg/server/controller/authorization.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
func HandleAuthorization(currentUser *acl.Session, tx *gorm.DB, action int, old, new *model.Asset) (err error) {
|
||||
ctx := context.Background()
|
||||
assetId := lo.TernaryF(new == nil, func() int { return old.Id }, func() int { return new.Id })
|
||||
mtx := &sync.Mutex{}
|
||||
eg := &errgroup.Group{}
|
||||
if action == model.ACTION_UPDATE {
|
||||
if sameAuthorization(old.Authorization, new.Authorization) {
|
||||
return
|
||||
}
|
||||
for id := range old.Authorization {
|
||||
if _, ok := new.Authorization[id]; ok {
|
||||
continue
|
||||
}
|
||||
accountId := id
|
||||
eg.Go(func() (err error) {
|
||||
a := &model.Authorization{}
|
||||
if err = mysql.DB.
|
||||
Model(a).
|
||||
Select("id", "resource_id").
|
||||
Where("asset_id = ? AND account_id = ?", assetId, accountId).
|
||||
First(a).
|
||||
Error; err != nil {
|
||||
return
|
||||
}
|
||||
if err = acl.DeleteResource(ctx, currentUser.GetUid(), a.ResourceId); err != nil {
|
||||
return
|
||||
}
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
err = tx.Delete(a, a.Id).Error
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
as := lo.TernaryF(action == model.ACTION_DELETE,
|
||||
func() model.Map[int, model.Slice[int]] { return old.Authorization },
|
||||
func() model.Map[int, model.Slice[int]] { return new.Authorization })
|
||||
for k, v := range as {
|
||||
accountId := k
|
||||
curRids := lo.Uniq(v)
|
||||
eg.Go(func() (err error) {
|
||||
resourceId := 0
|
||||
if err = mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Select("resource_id").
|
||||
Where("asset_id = ? AND account_id = ?", assetId, accountId).
|
||||
First(&resourceId).
|
||||
Error; err != nil {
|
||||
notFount := errors.Is(err, gorm.ErrRecordNotFound)
|
||||
if !notFount || (notFount && action == model.ACTION_DELETE) {
|
||||
return
|
||||
}
|
||||
if resourceId, err = acl.CreateGrantAcl(ctx, currentUser, conf.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION),
|
||||
fmt.Sprintf("%d-%d", assetId, accountId)); err != nil {
|
||||
return
|
||||
}
|
||||
mtx.Lock()
|
||||
if err = tx.Create(&model.Authorization{AssetId: assetId, AccountId: accountId, ResourceId: resourceId,
|
||||
CreatorId: currentUser.GetUid(), UpdaterId: currentUser.GetUid()}).Error; err != nil {
|
||||
return
|
||||
}
|
||||
mtx.Unlock()
|
||||
}
|
||||
switch action {
|
||||
case model.ACTION_CREATE:
|
||||
err = acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), curRids, resourceId, []string{acl.READ})
|
||||
case model.ACTION_DELETE:
|
||||
err = acl.DeleteResource(ctx, currentUser.GetUid(), resourceId)
|
||||
case model.ACTION_UPDATE:
|
||||
var res map[string]*acl.ResourcePermissionsRespItem
|
||||
res, err = acl.GetResourcePermissions(ctx, resourceId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
perms := make([]*acl.Perm, 0)
|
||||
for _, v := range res {
|
||||
perms = append(perms, v.Perms...)
|
||||
}
|
||||
preRids := lo.Map(lo.Filter(perms, func(p *acl.Perm, _ int) bool { return p.Name == acl.READ }), func(p *acl.Perm, _ int) int { return p.Rid })
|
||||
revokeRids := lo.Without(preRids, curRids...)
|
||||
if len(revokeRids) > 0 {
|
||||
if err = acl.BatchRevokeRoleResource(ctx, currentUser.GetUid(), revokeRids, resourceId, []string{acl.READ}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
grantRids := lo.Without(curRids, preRids...)
|
||||
if len(grantRids) > 0 {
|
||||
err = acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), grantRids, resourceId, []string{acl.READ})
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func sameAuthorization(old, new model.Map[int, model.Slice[int]]) bool {
|
||||
if len(old) != len(new) {
|
||||
return false
|
||||
}
|
||||
ks := lo.Uniq(append(lo.Keys(old), lo.Keys(new)...))
|
||||
for _, k := range ks {
|
||||
if len(old[k]) != len(new[k]) {
|
||||
return false
|
||||
}
|
||||
o, n := make([]int, 0, len(old[k])), make([]int, 0, len(new[k]))
|
||||
copy(o, old[k])
|
||||
copy(n, new[k])
|
||||
sort.Ints(o)
|
||||
sort.Ints(n)
|
||||
if !reflect.DeepEqual(o, n) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GetAutorizationResourceIds(ctx *gin.Context) (resourceIds []int, err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
var rs []*acl.Resource
|
||||
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, conf.RESOURCE_AUTHORIZATION)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resourceIds = lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })
|
||||
|
||||
return
|
||||
}
|
129
backend/pkg/server/controller/command.go
Normal file
129
backend/pkg/server/controller/command.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
commandDcs = []deleteCheck{
|
||||
func(ctx *gin.Context, id int) {
|
||||
assetName := ""
|
||||
err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("name").
|
||||
Where(fmt.Sprintf("JSON_CONTAINS(cmd_ids, '%d')", id)).
|
||||
First(&assetName).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return
|
||||
}
|
||||
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
|
||||
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
|
||||
ctx.AbortWithError(code, err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreateCommand godoc
|
||||
//
|
||||
// @Tags command
|
||||
// @Param command body model.Command true "command"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /command [post]
|
||||
func (c *Controller) CreateCommand(ctx *gin.Context) {
|
||||
doCreate(ctx, true, &model.Command{}, conf.RESOURCE_COMMAND)
|
||||
}
|
||||
|
||||
// DeleteCommand godoc
|
||||
//
|
||||
// @Tags command
|
||||
// @Param id path int true "command id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /command/:id [delete]
|
||||
func (c *Controller) DeleteCommand(ctx *gin.Context) {
|
||||
doDelete(ctx, true, &model.Command{}, commandDcs...)
|
||||
}
|
||||
|
||||
// UpdateCommand godoc
|
||||
//
|
||||
// @Tags command
|
||||
// @Param id path int true "command id"
|
||||
// @Param command body model.Command true "command"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /command/:id [put]
|
||||
func (c *Controller) UpdateCommand(ctx *gin.Context) {
|
||||
doUpdate(ctx, true, &model.Command{})
|
||||
}
|
||||
|
||||
// GetCommands godoc
|
||||
//
|
||||
// @Tags command
|
||||
// @Param page_index query int true "command id"
|
||||
// @Param page_size query int true "command id"
|
||||
// @Param search query string false "name or cmds"
|
||||
// @Param id query int false "command id"
|
||||
// @Param ids query string false "command ids"
|
||||
// @Param name query string false "command name"
|
||||
// @Param enable query int false "command enable"
|
||||
// @Param info query bool false "is info mode"
|
||||
// @Param search query string false "name or cmds"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Command}}
|
||||
// @Router /command [get]
|
||||
func (c *Controller) GetCommands(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
db := mysql.DB.Model(&model.Command{})
|
||||
db = filterEqual(ctx, db, "id", "enable")
|
||||
db = filterLike(ctx, db, "name")
|
||||
db = filterSearch(ctx, db, "name", "cmds")
|
||||
if q, ok := ctx.GetQuery("ids"); ok {
|
||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||
}
|
||||
|
||||
if info && !acl.IsAdmin(currentUser) {
|
||||
//rs := make([]*acl.Resource, 0)
|
||||
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
sub := mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Select("DISTINCT asset_id").
|
||||
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
|
||||
cmdIds := make([]model.Slice[int], 0)
|
||||
if err = mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("cmd_ids").
|
||||
Where("id IN (?)", sub).
|
||||
Find(&cmdIds).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
}
|
||||
|
||||
ids := make([]int, 0)
|
||||
for _, s := range cmdIds {
|
||||
ids = append(ids, s...)
|
||||
}
|
||||
|
||||
db = db.Where("id IN ?", lo.Uniq(ids))
|
||||
}
|
||||
|
||||
db = db.Order("name")
|
||||
|
||||
doGet[*model.Command](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_COMMAND))
|
||||
}
|
73
backend/pkg/server/controller/config.go
Normal file
73
backend/pkg/server/controller/config.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
// PostConfig godoc
|
||||
//
|
||||
// @Tags config
|
||||
// @Param command body model.Config true "config"
|
||||
// @Success 200 {object} HttpResponse{}
|
||||
// @Router /config [post]
|
||||
func (c *Controller) PostConfig(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := &model.Config{}
|
||||
if err := ctx.BindJSON(cfg); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
cfg.Id = 0
|
||||
cfg.CreatorId = currentUser.GetUid()
|
||||
cfg.UpdaterId = currentUser.GetUid()
|
||||
|
||||
if err := mysql.DB.Model(cfg).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("deleted_at = 0").Delete(&model.Config{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(cfg).Error
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
||||
|
||||
// GetConfig godoc
|
||||
//
|
||||
// @Tags config
|
||||
// @Param info query bool false "is info mode"
|
||||
// @Success 200 {object} HttpResponse{data=model.Config}
|
||||
// @Router /config [get]
|
||||
func (c *Controller) GetConfig(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
if !cast.ToBool(ctx.Query("info")) && !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
|
||||
return
|
||||
}
|
||||
|
||||
cfg := &model.Config{}
|
||||
if err := mysql.DB.Model(cfg).First(&cfg).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(cfg))
|
||||
}
|
573
backend/pkg/server/controller/connect.go
Normal file
573
backend/pkg/server/controller/connect.go
Normal file
@@ -0,0 +1,573 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
myi18n "github.com/veops/oneterm/pkg/i18n"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
Upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Connect godoc
|
||||
//
|
||||
// @Tags connect
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Param session_id path int true "session id"
|
||||
// @Router /connect/:session_id [get]
|
||||
func (c *Controller) Connecting(ctx *gin.Context) {
|
||||
sessionId := ctx.Param("session_id")
|
||||
|
||||
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logger.L.Debug("connecting failed", zap.String("session_id", sessionId), zap.Error(err))
|
||||
ae, ok := err.(*ApiError)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
lang := ctx.PostForm("lang")
|
||||
accept := ctx.GetHeader("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
|
||||
ws.WriteMessage(websocket.TextMessage, []byte(ae.Message(localizer)))
|
||||
}()
|
||||
|
||||
v, ok := onlineSession.Load(sessionId)
|
||||
if !ok {
|
||||
err = &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}}
|
||||
return
|
||||
}
|
||||
session, ok := v.(*model.Session)
|
||||
if !ok {
|
||||
err = &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "invalid type"}}
|
||||
return
|
||||
}
|
||||
if session.Connected.Load() {
|
||||
err = &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}}
|
||||
return
|
||||
}
|
||||
session.Connected.CompareAndSwap(false, true)
|
||||
|
||||
chs := session.Chans
|
||||
chs.WindowChan <- fmt.Sprintf("%s,%s", ctx.Query("w"), ctx.Query("h"))
|
||||
defer func() {
|
||||
close(chs.AwayChan)
|
||||
}()
|
||||
readWsErrChan := make(chan error)
|
||||
go func() {
|
||||
readWsErrChan <- readWsMsg(ctx, ws, chs)
|
||||
}()
|
||||
tk, tk1s := time.NewTicker(time.Millisecond*100), time.NewTicker(time.Second)
|
||||
defer sendMsg(ws, session, chs)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-readWsErrChan:
|
||||
logger.L.Error("websocket read failed", zap.Error(err))
|
||||
return
|
||||
case closeBy := <-chs.CloseChan:
|
||||
out := []byte("\r\n \033[31m closed by admin")
|
||||
ws.WriteMessage(websocket.TextMessage, out)
|
||||
writeToMonitors(session.Monitors, out)
|
||||
logger.L.Warn("close by admin", zap.String("username", closeBy))
|
||||
return
|
||||
case err := <-chs.ErrChan:
|
||||
logger.L.Error("ssh connection failed", zap.Error(err))
|
||||
return
|
||||
case in := <-chs.InChan:
|
||||
rt := in[0]
|
||||
msg := in[1:]
|
||||
switch rt {
|
||||
case '1':
|
||||
chs.Win.Write(msg)
|
||||
case '9':
|
||||
continue
|
||||
case 'w':
|
||||
chs.WindowChan <- string(msg)
|
||||
}
|
||||
case out := <-chs.OutChan:
|
||||
chs.Buf.Write(out)
|
||||
case <-tk.C:
|
||||
sendMsg(ws, session, chs)
|
||||
case <-tk1s.C:
|
||||
ws.WriteMessage(websocket.TextMessage, nil)
|
||||
writeToMonitors(session.Monitors, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendMsg(ws *websocket.Conn, session *model.Session, chs *model.SessionChans) {
|
||||
out := chs.Buf.Bytes()
|
||||
if len(out) <= 0 {
|
||||
return
|
||||
}
|
||||
if ws != nil {
|
||||
ws.WriteMessage(websocket.TextMessage, out)
|
||||
}
|
||||
writeToMonitors(session.Monitors, out)
|
||||
chs.Buf.Reset()
|
||||
}
|
||||
|
||||
// Connect godoc
|
||||
//
|
||||
// @Tags connect
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Param w query int false "width"
|
||||
// @Param h query int false "height"
|
||||
// @Success 200 {object} HttpResponse{data=model.Session}
|
||||
// @Router /connect/:asset_id/:account_id/:protocol [post]
|
||||
func (c *Controller) Connect(ctx *gin.Context) {
|
||||
chs := makeChans()
|
||||
|
||||
resp := &model.SshResp{}
|
||||
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), newSshReq(ctx, model.SESSIONACTION_NEW), chs)
|
||||
if err := <-chs.ErrChan; err != nil {
|
||||
logger.L.Error("failed to connect", zap.Error(err))
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
resp = <-chs.RespChan
|
||||
if resp.Code != 0 {
|
||||
logger.L.Error("failed to connect", zap.Any("resp", *resp))
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}})
|
||||
return
|
||||
}
|
||||
v, ok := onlineSession.Load(resp.SessionId)
|
||||
if !ok {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "cannot find in sync map"}})
|
||||
return
|
||||
}
|
||||
session, ok := v.(*model.Session)
|
||||
if !ok {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "invalid type"}})
|
||||
return
|
||||
}
|
||||
session.Chans = chs
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(session))
|
||||
}
|
||||
|
||||
func readWsMsg(ctx context.Context, ws *websocket.Conn, chs *model.SessionChans) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("ctx done")
|
||||
default:
|
||||
t, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(msg) <= 0 {
|
||||
logger.L.Warn("websocket msg length is zero")
|
||||
continue
|
||||
}
|
||||
switch t {
|
||||
case websocket.TextMessage:
|
||||
chs.InChan <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doSsh(ctx *gin.Context, w, h int, req *model.SshReq, chs *model.SessionChans) {
|
||||
var err error
|
||||
defer func() {
|
||||
chs.ErrChan <- err
|
||||
}()
|
||||
|
||||
cfg := &ssh.ClientConfig{
|
||||
User: conf.Cfg.SshServer.Account,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.Password(conf.Cfg.SshServer.Password),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", conf.Cfg.SshServer.Ip, conf.Cfg.SshServer.Port), cfg)
|
||||
if err != nil {
|
||||
logger.L.Error("ssh tcp dail failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
sess, err := conn.NewSession()
|
||||
if err != nil {
|
||||
logger.L.Error("ssh session create failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
rout, wout := io.Pipe()
|
||||
sess.Stdout = wout
|
||||
sess.Stderr = wout
|
||||
sess.Stdin = chs.Rin
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0,
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
if err = sess.RequestPty("xterm", h, w, modes); err != nil {
|
||||
logger.L.Error("ssh request pty failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if err = sess.Shell(); err != nil {
|
||||
logger.L.Error("ssh start shell failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
logger.L.Error("ssh req marshal failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if _, err = chs.Win.Write(append(bs, '\r')); err != nil {
|
||||
logger.L.Error("ssh req", zap.Error(err), zap.String("req content", string(bs)))
|
||||
return
|
||||
}
|
||||
|
||||
buf := bufio.NewReader(rout)
|
||||
|
||||
line, err := buf.ReadBytes('\r')
|
||||
if err != nil {
|
||||
logger.L.Error("ssh read bytes failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
resp := &model.SshResp{}
|
||||
if err = json.Unmarshal([]byte(line)[0:len(line)-1], resp); err != nil {
|
||||
logger.L.Error("ssh resp", zap.Error(err), zap.String("resp content", string(line)))
|
||||
return
|
||||
}
|
||||
|
||||
chs.ErrChan <- nil
|
||||
chs.RespChan <- resp
|
||||
|
||||
waitChan := make(chan error)
|
||||
go func() {
|
||||
waitChan <- sess.Wait()
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
rn, size, err := buf.ReadRune()
|
||||
if err != nil {
|
||||
logger.L.Debug("buf ReadRune failed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
if size <= 0 || rn == utf8.RuneError {
|
||||
continue
|
||||
}
|
||||
p := make([]byte, utf8.RuneLen(rn))
|
||||
utf8.EncodeRune(p, rn)
|
||||
chs.OutChan <- p
|
||||
}
|
||||
}()
|
||||
defer sess.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err = <-waitChan:
|
||||
return
|
||||
case <-chs.AwayChan:
|
||||
return
|
||||
case s := <-chs.WindowChan:
|
||||
wh := strings.Split(s, ",")
|
||||
if len(wh) < 2 {
|
||||
continue
|
||||
}
|
||||
w = cast.ToInt(wh[0])
|
||||
h = cast.ToInt(wh[1])
|
||||
if w <= 0 || h <= 0 {
|
||||
continue
|
||||
}
|
||||
if err := sess.WindowChange(h, w); err != nil {
|
||||
logger.L.Warn("reset window size failed", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeChans() *model.SessionChans {
|
||||
rin, win := io.Pipe()
|
||||
return &model.SessionChans{
|
||||
Rin: rin,
|
||||
Win: win,
|
||||
ErrChan: make(chan error),
|
||||
RespChan: make(chan *model.SshResp),
|
||||
InChan: make(chan []byte),
|
||||
OutChan: make(chan []byte),
|
||||
Buf: &bytes.Buffer{},
|
||||
WindowChan: make(chan string),
|
||||
AwayChan: make(chan struct{}),
|
||||
CloseChan: make(chan string),
|
||||
}
|
||||
}
|
||||
|
||||
func newSshReq(ctx *gin.Context, action int) *model.SshReq {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
return &model.SshReq{
|
||||
Uid: currentUser.GetUid(),
|
||||
UserName: currentUser.GetUserName(),
|
||||
Cookie: ctx.GetHeader("Cookie"),
|
||||
AcceptLanguage: ctx.GetHeader("Accept-Language"),
|
||||
ClientIp: ctx.ClientIP(),
|
||||
AssetId: cast.ToInt(ctx.Param("asset_id")),
|
||||
AccountId: cast.ToInt(ctx.Param("account_id")),
|
||||
Protocol: ctx.Param("protocol"),
|
||||
Action: action,
|
||||
SessionId: ctx.Param("session_id"),
|
||||
}
|
||||
}
|
||||
|
||||
func writeToMonitors(monitors *sync.Map, out []byte) {
|
||||
monitors.Range(func(key, value any) bool {
|
||||
ws, ok := value.(*websocket.Conn)
|
||||
if !ok || ws == nil {
|
||||
return true
|
||||
}
|
||||
ws.WriteMessage(websocket.TextMessage, out)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// ConnectMonitor godoc
|
||||
//
|
||||
// @Tags connect
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /connect/monitor/:session_id [get]
|
||||
func (c *Controller) ConnectMonitor(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
sessionId := ctx.Param("session_id")
|
||||
key := fmt.Sprintf("%d-%s-%d", currentUser.Uid, sessionId, time.Now().Nanosecond())
|
||||
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
defer func() {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logger.L.Debug("monitor failed", zap.String("session_id", sessionId), zap.Error(err))
|
||||
ae, ok := err.(*ApiError)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
lang := ctx.PostForm("lang")
|
||||
accept := ctx.GetHeader("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
|
||||
ws.WriteMessage(websocket.TextMessage, []byte(ae.Message(localizer)))
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
}()
|
||||
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": "monitor session"}})
|
||||
return
|
||||
}
|
||||
|
||||
session := &model.Session{}
|
||||
err = mysql.DB.
|
||||
Where("session_id = ?", sessionId).
|
||||
Where("status = ?", model.SESSIONSTATUS_ONLINE).
|
||||
First(session).
|
||||
Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
onlineSession.Delete(sessionId)
|
||||
}
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
|
||||
return
|
||||
}
|
||||
|
||||
v, ok := onlineSession.Load(sessionId)
|
||||
if !ok {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
|
||||
return
|
||||
}
|
||||
session, ok = v.(*model.Session)
|
||||
if !ok {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
|
||||
return
|
||||
}
|
||||
switch session.SessionType {
|
||||
case model.SESSIONTYPE_WEB:
|
||||
case model.SESSIONTYPE_CLIENT:
|
||||
cur := false
|
||||
session.Monitors.Range(func(key, value any) bool {
|
||||
cur = true
|
||||
return !cur
|
||||
})
|
||||
if !cur {
|
||||
req := newSshReq(ctx, model.SESSIONACTION_MONITOR)
|
||||
req.SessionId = sessionId
|
||||
chs := makeChans()
|
||||
logger.L.Debug("connect to monitor client", zap.String("sessionId", sessionId))
|
||||
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), req, chs)
|
||||
if err = <-chs.ErrChan; err != nil {
|
||||
err = &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}}
|
||||
return
|
||||
}
|
||||
resp := <-chs.RespChan
|
||||
if resp.Code != 0 {
|
||||
err = &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}}
|
||||
return
|
||||
}
|
||||
tk := time.NewTicker(time.Millisecond * 100)
|
||||
defer sendMsg(nil, session, chs)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case closeBy := <-chs.CloseChan:
|
||||
writeToMonitors(session.Monitors, []byte("\r\n \033[31m closed by admin"))
|
||||
logger.L.Warn("close by admin", zap.String("username", closeBy))
|
||||
return
|
||||
case err := <-chs.ErrChan:
|
||||
logger.L.Error("ssh connection failed", zap.Error(err))
|
||||
return
|
||||
case out := <-chs.OutChan:
|
||||
chs.Buf.Write(out)
|
||||
case <-tk.C:
|
||||
sendMsg(nil, session, chs)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
session.Monitors.Store(key, ws)
|
||||
defer func() {
|
||||
session.Monitors.Delete(key)
|
||||
}()
|
||||
for {
|
||||
_, _, err = ws.ReadMessage()
|
||||
if err != nil {
|
||||
logger.L.Warn("end monitor", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectClose godoc
|
||||
//
|
||||
// @Tags connect
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /connect/close/:session_id [post]
|
||||
func (c *Controller) ConnectClose(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": "close session"}})
|
||||
return
|
||||
}
|
||||
|
||||
session := &model.Session{}
|
||||
err := mysql.DB.
|
||||
Model(session).
|
||||
Where("session_id = ?", ctx.Param("session_id")).
|
||||
Where("status = ?", model.SESSIONSTATUS_ONLINE).
|
||||
First(session).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": "invalid session id"}})
|
||||
return
|
||||
}
|
||||
|
||||
logger.L.Info("closing...", zap.String("sessionId", session.SessionId), zap.Int("type", session.SessionType))
|
||||
defer doOfflineOnlineSession(ctx, session.SessionId, currentUser.GetUserName())
|
||||
chs := makeChans()
|
||||
req := newSshReq(ctx, model.SESSIONACTION_CLOSE)
|
||||
req.SessionId = session.SessionId
|
||||
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), req, chs)
|
||||
if err = <-chs.ErrChan; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
resp := <-chs.RespChan
|
||||
if resp.Code != 0 {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrBadRequest, Data: map[string]any{"err": resp.Message}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
||||
|
||||
func doOfflineOnlineSession(ctx *gin.Context, sessionId string, closer string) {
|
||||
logger.L.Debug("offline", zap.String("session_id", sessionId), zap.String("closer", closer))
|
||||
defer onlineSession.Delete(sessionId)
|
||||
v, ok := onlineSession.Load(sessionId)
|
||||
if ok {
|
||||
if session, ok := v.(*model.Session); ok {
|
||||
if closer != "" && session.Chans != nil {
|
||||
select {
|
||||
case session.Chans.CloseChan <- closer:
|
||||
break
|
||||
case <-time.After(time.Second):
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
session.Monitors.Range(func(key, value any) bool {
|
||||
ws, ok := value.(*websocket.Conn)
|
||||
if ok && ws != nil {
|
||||
lang := ctx.PostForm("lang")
|
||||
accept := ctx.GetHeader("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
|
||||
cfg := &i18n.LocalizeConfig{
|
||||
TemplateData: map[string]any{"sessionId": sessionId},
|
||||
DefaultMessage: myi18n.MsgSessionEnd,
|
||||
}
|
||||
msg, _ := localizer.Localize(cfg)
|
||||
ws.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||
ws.Close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
439
backend/pkg/server/controller/controller.go
Normal file
439
backend/pkg/server/controller/controller.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/remote"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultHttpResponse = &HttpResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: nil,
|
||||
}
|
||||
)
|
||||
|
||||
type preHook[T any] func(*gin.Context, T)
|
||||
type postHook[T any] func(*gin.Context, []T)
|
||||
type deleteCheck func(*gin.Context, int)
|
||||
|
||||
type Controller struct{}
|
||||
|
||||
func NewController() *Controller {
|
||||
return &Controller{}
|
||||
}
|
||||
|
||||
type HttpResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type ListData struct {
|
||||
Count int64 `json:"count"`
|
||||
List []any `json:"list"`
|
||||
}
|
||||
|
||||
func NewHttpResponseWithData(data any) *HttpResponse {
|
||||
return &HttpResponse{
|
||||
Code: 0,
|
||||
Message: "ok",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func doCreate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, preHooks ...preHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
if err = ctx.BindJSON(md); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
for _, hook := range preHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, md)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resourceId := 0
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
||||
return
|
||||
}
|
||||
resourceId, err = acl.CreateGrantAcl(ctx, currentUser, resourceType, md.GetName())
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
md.SetResourceId(resourceId)
|
||||
}
|
||||
|
||||
md.SetCreatorId(currentUser.Uid)
|
||||
md.SetUpdaterId(currentUser.Uid)
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
if err = tx.Model(md).Create(md).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_CREATE, nil, t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Create(&model.History{
|
||||
RemoteIp: ctx.ClientIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_CREATE,
|
||||
Old: nil,
|
||||
New: toMap(md),
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doDelete[T model.Model](ctx *gin.Context, needAcl bool, md T, dcs ...deleteCheck) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
id, err := cast.ToIntE(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = mysql.DB.Model(md).Where("id = ?", id).First(md).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.DELETE) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.DELETE}})
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, dc := range dcs {
|
||||
if dc == nil {
|
||||
continue
|
||||
}
|
||||
dc(ctx, id)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
if needAcl {
|
||||
if err = acl.DeleteResource(ctx, currentUser.GetUid(), md.GetResourceId()); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_DELETE, t, nil); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Delete(md, id).Error; err != nil {
|
||||
return
|
||||
}
|
||||
err = tx.Create(&model.History{
|
||||
RemoteIp: ctx.ClientIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_DELETE,
|
||||
Old: toMap(md),
|
||||
New: nil,
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
return
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, preHooks ...preHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
id, err := cast.ToIntE(ctx.Param("id"))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.BindJSON(md); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
md.SetUpdaterId(currentUser.Uid)
|
||||
|
||||
for _, hook := range preHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, md)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
old := getEmpty(md)
|
||||
if err = mysql.DB.Model(md).Where("id = ?", id).First(old).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if needAcl {
|
||||
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.WRITE) {
|
||||
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
|
||||
return
|
||||
}
|
||||
|
||||
if err = acl.UpdateResource(ctx, currentUser.GetUid(), old.GetResourceId(), map[string]string{"name": md.GetName()}); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
md.SetId(id)
|
||||
|
||||
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
|
||||
omits := []string{"resource_id", "created_at", "deleted_at"}
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = HandleAuthorization(currentUser, tx, model.ACTION_UPDATE, any(old).(*model.Asset), t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
omits = append(omits, "ci_id")
|
||||
}
|
||||
|
||||
if err = mysql.DB.Omit(omits...).Save(md).Error; err != nil {
|
||||
return
|
||||
}
|
||||
err = mysql.DB.Create(&model.History{
|
||||
RemoteIp: ctx.ClientIP(),
|
||||
Type: md.TableName(),
|
||||
TargetId: md.GetId(),
|
||||
ActionType: model.ACTION_UPDATE,
|
||||
Old: toMap(old),
|
||||
New: toMap(md),
|
||||
CreatorId: currentUser.Uid,
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
return
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func doGet[T any](ctx *gin.Context, needAcl bool, dbFind *gorm.DB, resourceType string, postHooks ...postHook[T]) (err error) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
if needAcl && !acl.IsAdmin(currentUser) {
|
||||
//rs := make([]*acl.Resource, 0)
|
||||
var rs []*acl.Resource
|
||||
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, resourceType)
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
dbFind = dbFind.Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
|
||||
}
|
||||
|
||||
dbCount := dbFind.Session(&gorm.Session{})
|
||||
dbFind = dbFind.Session(&gorm.Session{})
|
||||
|
||||
count, list := int64(0), make([]T, 0)
|
||||
eg := &errgroup.Group{}
|
||||
eg.Go(func() error {
|
||||
return dbCount.Count(&count).
|
||||
Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
pi, ps := cast.ToInt(ctx.Query("page_index")), cast.ToInt(ctx.Query("page_size"))
|
||||
if _, ok := ctx.GetQuery("page_index"); ok {
|
||||
dbFind = dbFind.Offset((pi - 1) * ps)
|
||||
}
|
||||
if _, ok := ctx.GetQuery("page_size"); ok {
|
||||
dbFind = dbFind.Limit(ps)
|
||||
}
|
||||
return dbFind.
|
||||
Order("id DESC").
|
||||
Find(&list).
|
||||
Error
|
||||
})
|
||||
if err = eg.Wait(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
for _, hook := range postHooks {
|
||||
if hook == nil {
|
||||
continue
|
||||
}
|
||||
hook(ctx, list)
|
||||
if ctx.IsAborted() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res := &ListData{
|
||||
Count: count,
|
||||
List: lo.Map(list, func(t T, _ int) any { return t }),
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleRemoteErr(ctx *gin.Context, err error) {
|
||||
switch e := err.(type) {
|
||||
case *remote.RemoteError:
|
||||
if e.HttpCode == http.StatusBadRequest {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrRemoteClient, Data: e.Resp})
|
||||
return
|
||||
}
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrRemoteServer, Data: e.Resp})
|
||||
default:
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
}
|
||||
}
|
||||
|
||||
func filterSearch(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
q, ok := ctx.GetQuery("search")
|
||||
if !ok || len(fields) <= 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
d := mysql.DB
|
||||
for _, f := range fields {
|
||||
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
|
||||
}
|
||||
|
||||
db = db.Where(d)
|
||||
|
||||
return db
|
||||
}
|
||||
func filterStartEnd(ctx *gin.Context, db *gorm.DB, fields ...string) (*gorm.DB, error) {
|
||||
if q, ok := ctx.GetQuery("start"); ok {
|
||||
t, err := time.Parse(time.RFC3339, q)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
return db, err
|
||||
}
|
||||
db = db.Where("created_at >= ?", t)
|
||||
}
|
||||
if q, ok := ctx.GetQuery("end"); ok {
|
||||
t, err := time.Parse(time.RFC3339, q)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
return db, err
|
||||
}
|
||||
db = db.Where("created_at <= ?", t)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
func filterEqual(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
for _, f := range fields {
|
||||
if q, ok := ctx.GetQuery(f); ok {
|
||||
db = db.Where(fmt.Sprintf("%s = ?", f), q)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
func filterLike(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
|
||||
likes := false
|
||||
d := mysql.DB
|
||||
for _, f := range fields {
|
||||
if q, ok := ctx.GetQuery(f); ok && q != "" {
|
||||
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
|
||||
likes = true
|
||||
}
|
||||
}
|
||||
if !likes {
|
||||
return db
|
||||
}
|
||||
db = db.Where(d)
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func toMap(data any) model.Map[string, any] {
|
||||
bs, _ := json.Marshal(data)
|
||||
res := make(map[string]any)
|
||||
json.Unmarshal(bs, &res)
|
||||
return res
|
||||
}
|
||||
|
||||
func getEmpty[T model.Model](data T) T {
|
||||
t := reflect.TypeOf(data).Elem()
|
||||
v := reflect.New(t)
|
||||
return v.Interface().(T)
|
||||
}
|
68
backend/pkg/server/controller/errors.go
Normal file
68
backend/pkg/server/controller/errors.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
myi18n "github.com/veops/oneterm/pkg/i18n"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrBadRequest = 4000
|
||||
ErrInvalidArgument = 4001
|
||||
ErrDuplicateName = 4002
|
||||
ErrHasChild = 4003
|
||||
ErrHasDepency = 4004
|
||||
ErrNoPerm = 4005
|
||||
ErrRemoteClient = 4006
|
||||
ErrWrongPk = 4007
|
||||
ErrWrongMac = 4008
|
||||
ErrInvalidSessionId = 4009
|
||||
ErrInternal = 5000
|
||||
ErrRemoteServer = 5001
|
||||
ErrConnectServer = 5002
|
||||
ErrLoadSession = 5003
|
||||
)
|
||||
|
||||
var (
|
||||
Err2Msg = map[int]*i18n.Message{
|
||||
ErrBadRequest: myi18n.MsgBadRequest,
|
||||
ErrInvalidArgument: myi18n.MsgInvalidArguemnt,
|
||||
ErrDuplicateName: myi18n.MsgDupName,
|
||||
ErrHasChild: myi18n.MsgHasChild,
|
||||
ErrHasDepency: myi18n.MsgHasDepdency,
|
||||
ErrNoPerm: myi18n.MsgNoPerm,
|
||||
ErrRemoteClient: myi18n.MsgRemoteClient,
|
||||
ErrWrongPk: myi18n.MsgWrongPk,
|
||||
ErrInvalidSessionId: myi18n.MsgInvalidSessionId,
|
||||
ErrInternal: myi18n.MsgInternalError,
|
||||
ErrRemoteServer: myi18n.MsgRemoteServer,
|
||||
ErrConnectServer: myi18n.MsgConnectServer,
|
||||
ErrLoadSession: myi18n.MsgLoadSession,
|
||||
}
|
||||
)
|
||||
|
||||
type ApiError struct {
|
||||
Code int
|
||||
Data map[string]any
|
||||
}
|
||||
|
||||
func (ae *ApiError) Error() string {
|
||||
return fmt.Sprintf("code=%d data=%v", ae.Code, ae.Data)
|
||||
}
|
||||
|
||||
func (ae *ApiError) Message(localizer *i18n.Localizer) (msg string) {
|
||||
cfg := &i18n.LocalizeConfig{}
|
||||
cfg.TemplateData = ae.Data
|
||||
m, ok := Err2Msg[ae.Code]
|
||||
if !ok {
|
||||
msg = ae.Error()
|
||||
return
|
||||
}
|
||||
cfg.DefaultMessage = m
|
||||
|
||||
msg, _ = localizer.Localize(cfg)
|
||||
|
||||
return
|
||||
}
|
173
backend/pkg/server/controller/gateway.go
Normal file
173
backend/pkg/server/controller/gateway.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
"github.com/veops/oneterm/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
gatewayPreHooks = []preHook[*model.Gateway]{
|
||||
func(ctx *gin.Context, data *model.Gateway) {
|
||||
if data.AccountType == model.AUTHMETHOD_PUBLICKEY {
|
||||
if data.Phrase == "" {
|
||||
_, err := ssh.ParsePrivateKey([]byte(data.Pk))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err := ssh.ParsePrivateKeyWithPassphrase([]byte(data.Pk), []byte(data.Phrase))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data *model.Gateway) {
|
||||
data.Password = util.EncryptAES(data.Password)
|
||||
data.Pk = util.EncryptAES(data.Pk)
|
||||
data.Phrase = util.EncryptAES(data.Phrase)
|
||||
},
|
||||
}
|
||||
gatewayPostHooks = []postHook[*model.Gateway]{
|
||||
func(ctx *gin.Context, data []*model.Gateway) {
|
||||
post := make([]*model.GatewayCount, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("gateway_id AS id, COUNT(*) AS count").
|
||||
Where("gateway_id IN ?", lo.Map(data, func(d *model.Gateway, _ int) int { return d.Id })).
|
||||
Group("gateway_id").
|
||||
Find(&post).
|
||||
Error; err != nil {
|
||||
return
|
||||
}
|
||||
m := lo.SliceToMap(post, func(p *model.GatewayCount) (int, int64) { return p.Id, p.Count })
|
||||
for _, d := range data {
|
||||
d.AssetCount = m[d.Id]
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data []*model.Gateway) {
|
||||
for _, d := range data {
|
||||
d.Password = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Password), "")
|
||||
d.Pk = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Pk), "")
|
||||
d.Phrase = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Phrase), "")
|
||||
}
|
||||
},
|
||||
}
|
||||
gatewayDcs = []deleteCheck{
|
||||
func(ctx *gin.Context, id int) {
|
||||
assetName := ""
|
||||
err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("name").
|
||||
Where("gateway_id = ?", id).
|
||||
First(&assetName).
|
||||
Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return
|
||||
}
|
||||
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
|
||||
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
|
||||
ctx.AbortWithError(code, err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreateGateway godoc
|
||||
//
|
||||
// @Tags gateway
|
||||
// @Param gateway body model.Gateway true "gateway"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /gateway [post]
|
||||
func (c *Controller) CreateGateway(ctx *gin.Context) {
|
||||
doCreate(ctx, true, &model.Gateway{}, conf.RESOURCE_GATEWAY, gatewayPreHooks...)
|
||||
}
|
||||
|
||||
// DeleteGateway godoc
|
||||
//
|
||||
// @Tags gateway
|
||||
// @Param id path int true "gateway id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /gateway/:id [delete]
|
||||
func (c *Controller) DeleteGateway(ctx *gin.Context) {
|
||||
doDelete(ctx, true, &model.Gateway{}, gatewayDcs...)
|
||||
}
|
||||
|
||||
// UpdateGateway godoc
|
||||
//
|
||||
// @Tags gateway
|
||||
// @Param id path int true "gateway id"
|
||||
// @Param gateway body model.Gateway true "gateway"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /gateway/:id [put]
|
||||
func (c *Controller) UpdateGateway(ctx *gin.Context) {
|
||||
doUpdate(ctx, true, &model.Gateway{}, gatewayPreHooks...)
|
||||
}
|
||||
|
||||
// GetGateways godoc
|
||||
//
|
||||
// @Tags gateway
|
||||
// @Param page_index query int true "gateway id"
|
||||
// @Param page_size query int true "gateway id"
|
||||
// @Param search query string false "name or host or account or port"
|
||||
// @Param id query int false "gateway id"
|
||||
// @Param ids query string false "gateway ids"
|
||||
// @Param name query string false "gateway name"
|
||||
// @Param info query bool false "is info mode"
|
||||
// @Param type query int false "account type"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Gateway}}
|
||||
// @Router /gateway [get]
|
||||
func (c *Controller) GetGateways(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
info := cast.ToBool(ctx.Query("info"))
|
||||
|
||||
db := mysql.DB.Model(&model.Gateway{})
|
||||
db = filterEqual(ctx, db, "id","type")
|
||||
db = filterLike(ctx, db, "name")
|
||||
db = filterSearch(ctx, db, "name", "host", "account","port")
|
||||
if q, ok := ctx.GetQuery("ids"); ok {
|
||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||
}
|
||||
|
||||
if info && !acl.IsAdmin(currentUser) {
|
||||
rs := make([]*acl.Resource, 0)
|
||||
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
|
||||
if err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
sub := mysql.DB.
|
||||
Model(&model.Authorization{}).
|
||||
Select("DISTINCT asset_id").
|
||||
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
|
||||
ids := make([]int, 0)
|
||||
if err = mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Where("id IN (?)", sub).
|
||||
Distinct().
|
||||
Pluck("gateway_id", &ids).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
}
|
||||
|
||||
db = db.Where("id IN ?", ids)
|
||||
}
|
||||
|
||||
db = db.Order("name")
|
||||
|
||||
doGet[*model.Gateway](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_GATEWAY), gatewayPostHooks...)
|
||||
}
|
67
backend/pkg/server/controller/history.go
Normal file
67
backend/pkg/server/controller/history.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
myi18n "github.com/veops/oneterm/pkg/i18n"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
// GetHistories godoc
|
||||
//
|
||||
// @Tags history
|
||||
// @Param page_index query int true "page_index"
|
||||
// @Param page_size query int true "page_size"
|
||||
// @Param type query string false "type" Enums(account, asset, command, gateway, node, public_key)
|
||||
// @Param target_id query int false "target_id"
|
||||
// @Param uid query int false "uid"
|
||||
// @Param action_type query int false "create=1 delete=2 update=3"
|
||||
// @Param start query string false "start time, RFC3339"
|
||||
// @Param end query string false "end time, RFC3339"
|
||||
// @Param search query string false ""
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.History}}
|
||||
// @Router /history [get]
|
||||
func (c *Controller) GetHistories(ctx *gin.Context) {
|
||||
db := mysql.DB.Model(&model.History{})
|
||||
db = filterSearch(ctx, db, "old", "new")
|
||||
db, err := filterStartEnd(ctx, db)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db = filterEqual(ctx, db, "type", "target_id", "action_type", "uid")
|
||||
|
||||
doGet[*model.History](ctx, false, db, "")
|
||||
}
|
||||
|
||||
// GetSessions godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Success 200 {object} HttpResponse{data=map[string]string}
|
||||
// @Router /history/type/mapping [get]
|
||||
func (c *Controller) GetHistoryTypeMapping(ctx *gin.Context) {
|
||||
lang := ctx.PostForm("lang")
|
||||
accept := ctx.GetHeader("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
|
||||
cfg := &i18n.LocalizeConfig{}
|
||||
key2msg := map[string]*i18n.Message{
|
||||
"account": myi18n.MsgTypeMappingAccount,
|
||||
"asset": myi18n.MsgTypeMappingAsset,
|
||||
"command": myi18n.MsgTypeMappingCommand,
|
||||
"gateway": myi18n.MsgTypeMappingGateway,
|
||||
"node": myi18n.MsgTypeMappingNode,
|
||||
"public_key": myi18n.MsgTypeMappingPublicKey,
|
||||
}
|
||||
data := make(map[string]string)
|
||||
for k, v := range key2msg {
|
||||
cfg.DefaultMessage = v
|
||||
msg, _ := localizer.Localize(cfg)
|
||||
data[k] = msg
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(data))
|
||||
}
|
217
backend/pkg/server/controller/node.go
Normal file
217
backend/pkg/server/controller/node.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
nodePreHooks = []preHook[*model.Node]{
|
||||
func(ctx *gin.Context, data *model.Node) {
|
||||
ids := make([]int, 0)
|
||||
if err := mysql.DB.Raw(fmt.Sprintf(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT id
|
||||
FROM node
|
||||
WHERE id=%s AND deleted_at = 0
|
||||
UNION ALL
|
||||
SELECT t.id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.id = t.parent_id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT
|
||||
id
|
||||
FROM cte
|
||||
`, ctx.Param("id"))).
|
||||
Find(&ids).
|
||||
Error; err != nil || lo.Contains(ids, data.ParentId) {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument})
|
||||
}
|
||||
},
|
||||
}
|
||||
nodePostHooks = []postHook[*model.Node]{
|
||||
func(ctx *gin.Context, data []*model.Node) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
isAdmin := acl.IsAdmin(currentUser)
|
||||
post := make([]*model.NodeCount, 0)
|
||||
sql := fmt.Sprintf(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT parent_id
|
||||
FROM asset
|
||||
%s
|
||||
UNION ALL
|
||||
SELECT t.parent_id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.parent_id = t.id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT
|
||||
parent_id,
|
||||
COUNT(*) AS count
|
||||
FROM cte
|
||||
GROUP BY parent_id
|
||||
`, lo.Ternary(isAdmin, "WHERE deleted_at = 0", "WHERE deleted_at = 0 AND id IN (?)"))
|
||||
db := mysql.DB.
|
||||
Model(&model.Asset{})
|
||||
if isAdmin {
|
||||
db = db.Raw(sql)
|
||||
} else {
|
||||
authorizationResourceIds, err := GetAutorizationResourceIds(ctx)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
db = db.Raw(sql, mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("resource_id IN ?", authorizationResourceIds))
|
||||
}
|
||||
if err := db.
|
||||
Find(&post).
|
||||
Error; err != nil {
|
||||
logger.L.Error("node posthookfailed asset count", zap.Error(err))
|
||||
return
|
||||
}
|
||||
m := lo.SliceToMap(post, func(p *model.NodeCount) (int, int64) { return p.ParentId, p.Count })
|
||||
for _, d := range data {
|
||||
d.AssetCount = m[d.Id]
|
||||
}
|
||||
}, func(ctx *gin.Context, data []*model.Node) {
|
||||
ps := make([]int, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Node{}).
|
||||
Where("parent_id IN ?", lo.Map(data, func(n *model.Node, _ int) int { return n.Id })).
|
||||
Pluck("parent_id", &ps).
|
||||
Error; err != nil {
|
||||
logger.L.Error("node posthookfailed has child", zap.Error(err))
|
||||
return
|
||||
}
|
||||
pm := lo.SliceToMap(ps, func(pid int) (int, bool) { return pid, true })
|
||||
for _, n := range data {
|
||||
n.HasChild = pm[n.Id]
|
||||
}
|
||||
},
|
||||
}
|
||||
nodeDcs = []deleteCheck{
|
||||
func(ctx *gin.Context, id int) {
|
||||
noChild := true
|
||||
noChild = noChild && errors.Is(mysql.DB.Model(&model.Node{}).Select("id").Where("parent_id = ?", id).First(map[string]any{}).Error, gorm.ErrRecordNotFound)
|
||||
noChild = noChild && errors.Is(mysql.DB.Model(&model.Asset{}).Select("id").Where("parent_id = ?", id).First(map[string]any{}).Error, gorm.ErrRecordNotFound)
|
||||
if noChild {
|
||||
return
|
||||
}
|
||||
|
||||
err := &ApiError{Code: ErrHasChild, Data: nil}
|
||||
ctx.AbortWithError(http.StatusBadRequest, err)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreateNode godoc
|
||||
//
|
||||
// @Tags node
|
||||
// @Param node body model.Node true "node"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /node [post]
|
||||
func (c *Controller) CreateNode(ctx *gin.Context) {
|
||||
doCreate(ctx, false, &model.Node{}, "")
|
||||
}
|
||||
|
||||
// DeleteNode godoc
|
||||
//
|
||||
// @Tags node
|
||||
// @Param id path int true "node id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /node/:id [delete]
|
||||
func (c *Controller) DeleteNode(ctx *gin.Context) {
|
||||
doDelete(ctx, false, &model.Node{}, nodeDcs...)
|
||||
}
|
||||
|
||||
// UpdateNode godoc
|
||||
//
|
||||
// @Tags node
|
||||
// @Param id path int true "node id"
|
||||
// @Param node body model.Node true "node"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /node/:id [put]
|
||||
func (c *Controller) UpdateNode(ctx *gin.Context) {
|
||||
doUpdate(ctx, false, &model.Node{}, nodePreHooks...)
|
||||
}
|
||||
|
||||
// GetNodes godoc
|
||||
//
|
||||
// @Tags node
|
||||
// @Param page_index query int true "node id"
|
||||
// @Param page_size query int true "node id"
|
||||
// @Param id query int false "node id"
|
||||
// @Param ids query string false "node ids"
|
||||
// @Param parent_id query int false "node's parent id"
|
||||
// @Param name query string false "node name"
|
||||
// @Param no_self_child query int false "exclude itself and its child"
|
||||
// @Param self_parent query int false "include itself and its parent"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Node}}
|
||||
// @Router /node [get]
|
||||
func (c *Controller) GetNodes(ctx *gin.Context) {
|
||||
db := mysql.DB.Model(&model.Node{})
|
||||
|
||||
db = filterEqual(ctx, db, "id", "parent_id")
|
||||
db = filterLike(ctx, db, "name")
|
||||
db = filterSearch(ctx, db, "name")
|
||||
if q, ok := ctx.GetQuery("ids"); ok {
|
||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||
}
|
||||
if id, ok := ctx.GetQuery("no_self_child"); ok {
|
||||
sql := fmt.Sprintf(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT id
|
||||
FROM node
|
||||
WHERE id=%s AND deleted_at = 0
|
||||
UNION ALL
|
||||
SELECT t.id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.id = t.parent_id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT
|
||||
id
|
||||
FROM cte
|
||||
`, id)
|
||||
sub := mysql.DB.Raw(sql)
|
||||
db = db.Where("id NOT IN (?)", sub)
|
||||
}
|
||||
|
||||
if id, ok := ctx.GetQuery("self_parent"); ok {
|
||||
sql := fmt.Sprintf(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT id,parent_id
|
||||
FROM node
|
||||
WHERE id=%s AND deleted_at = 0
|
||||
UNION ALL
|
||||
SELECT t.id,t.parent_id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.parent_id = t.id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT
|
||||
id
|
||||
FROM cte
|
||||
`, id)
|
||||
sub := mysql.DB.Raw(sql)
|
||||
db = db.Where("id IN (?)", sub)
|
||||
}
|
||||
|
||||
db = db.Order("name DESC")
|
||||
|
||||
doGet[*model.Node](ctx, false, db, "", nodePostHooks...)
|
||||
}
|
140
backend/pkg/server/controller/publicKey.go
Normal file
140
backend/pkg/server/controller/publicKey.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
"github.com/veops/oneterm/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
publicKeyPreHooks = []preHook[*model.PublicKey]{
|
||||
func(ctx *gin.Context, data *model.PublicKey) {
|
||||
if _, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(data.Pk)); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
|
||||
} else {
|
||||
data.Pk = strings.TrimSpace(strings.TrimSuffix(data.Pk, comment))
|
||||
}
|
||||
if _, err := net.ParseMAC(data.Mac); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongMac, Data: nil})
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data *model.PublicKey) {
|
||||
data.Pk = util.EncryptAES(data.Pk)
|
||||
},
|
||||
func(ctx *gin.Context, data *model.PublicKey) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
data.Uid = currentUser.GetUid()
|
||||
data.UserName = currentUser.GetUserName()
|
||||
},
|
||||
}
|
||||
publicKeyPostHooks = []postHook[*model.PublicKey]{
|
||||
func(ctx *gin.Context, data []*model.PublicKey) {
|
||||
for _, d := range data {
|
||||
d.Pk = util.DecryptAES(d.Pk)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// CreatePublicKey godoc
|
||||
//
|
||||
// @Tags public_key
|
||||
// @Param publicKey body model.PublicKey true "publicKey"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /public_key [post]
|
||||
func (c *Controller) CreatePublicKey(ctx *gin.Context) {
|
||||
doCreate(ctx, false, &model.PublicKey{}, "", publicKeyPreHooks...)
|
||||
}
|
||||
|
||||
// DeletePublicKey godoc
|
||||
//
|
||||
// @Tags public_key
|
||||
// @Param id path int true "publicKey id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /public_key/:id [delete]
|
||||
func (c *Controller) DeletePublicKey(ctx *gin.Context) {
|
||||
doDelete(ctx, false, &model.PublicKey{})
|
||||
}
|
||||
|
||||
// UpdatePublicKey godoc
|
||||
//
|
||||
// @Tags public_key
|
||||
// @Param id path int true "publicKey id"
|
||||
// @Param publicKey body model.PublicKey true "publicKey"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /public_key/:id [put]
|
||||
func (c *Controller) UpdatePublicKey(ctx *gin.Context) {
|
||||
doUpdate(ctx, false, &model.PublicKey{}, publicKeyPreHooks...)
|
||||
}
|
||||
|
||||
// GetPublicKeys godoc
|
||||
//
|
||||
// @Tags public_key
|
||||
// @Param page_index query int true "publicKey id"
|
||||
// @Param page_size query int true "publicKey id"
|
||||
// @Param search query string false "name or mac"
|
||||
// @Param id query int false "publicKey id"
|
||||
// @Param name query string false "publicKey name"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.PublicKey}}
|
||||
// @Router /public_key [get]
|
||||
func (c *Controller) GetPublicKeys(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
db := mysql.DB.Model(&model.PublicKey{})
|
||||
db = filterSearch(ctx, db, "name", "mac")
|
||||
db = filterEqual(ctx, db, "id")
|
||||
db = filterLike(ctx, db, "name")
|
||||
|
||||
db = db.Where("uid = ?", currentUser.Uid)
|
||||
|
||||
doGet[*model.PublicKey](ctx, false, db, "", publicKeyPostHooks...)
|
||||
}
|
||||
|
||||
// Auth godoc
|
||||
//
|
||||
// @Tags public_key
|
||||
// @Param req body model.ReqAuth false "method 1password 2publickey"
|
||||
// @Success 200 {object} HttpResponse{}
|
||||
// @Router /public_key/auth [post]
|
||||
func (c *Controller) Auth(ctx *gin.Context) {
|
||||
data := &model.ReqAuth{}
|
||||
if err := ctx.BindJSON(data); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Method {
|
||||
case model.AUTHMETHOD_PASSWORD:
|
||||
cookie, err := acl.LoginByPassword(ctx, data.UserName, data.Password)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusUnauthorized, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(map[string]any{"cookie": cookie}))
|
||||
case model.AUTHMETHOD_PUBLICKEY:
|
||||
pk := &model.PublicKey{}
|
||||
if err := mysql.DB.
|
||||
Where("username = ? AND pk = ?", data.UserName, util.EncryptAES(data.Pk)).
|
||||
First(pk).Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
cookie, err := acl.LoginByPublicKey(ctx, data.UserName)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusUnauthorized, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(map[string]any{"cookie": cookie}))
|
||||
default:
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": "invalid auth method"}})
|
||||
return
|
||||
}
|
||||
}
|
270
backend/pkg/server/controller/session.go
Normal file
270
backend/pkg/server/controller/session.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
var (
|
||||
onlineSession = &sync.Map{}
|
||||
|
||||
sessionPostHooks = []postHook[*model.Session]{
|
||||
func(ctx *gin.Context, data []*model.Session) {
|
||||
sessionIds := lo.Map(data, func(d *model.Session, _ int) string { return d.SessionId })
|
||||
if len(sessionIds) <= 0 {
|
||||
return
|
||||
}
|
||||
post := make([]*model.CmdCount, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.SessionCmd{}).
|
||||
Select("session_id, COUNT(*) AS count").
|
||||
Where("session_id IN ?", sessionIds).
|
||||
Group("session_id").
|
||||
Find(&post).
|
||||
Error; err != nil {
|
||||
logger.L.Error("gateway posthookfailed", zap.Error(err))
|
||||
return
|
||||
}
|
||||
m := lo.SliceToMap(post, func(p *model.CmdCount) (string, int64) { return p.SessionId, p.Count })
|
||||
for _, d := range data {
|
||||
d.CmdCount = m[d.SessionId]
|
||||
}
|
||||
},
|
||||
func(ctx *gin.Context, data []*model.Session) {
|
||||
now := time.Now()
|
||||
for _, d := range data {
|
||||
t := now
|
||||
if d.ClosedAt != nil {
|
||||
t = *d.ClosedAt
|
||||
}
|
||||
d.Duration = int64(t.Sub(d.CreatedAt).Seconds())
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func Init() (err error) {
|
||||
sessions := make([]*model.Session, 0)
|
||||
err = mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Where("status = ?", model.SESSIONSTATUS_ONLINE).
|
||||
Find(&sessions).
|
||||
Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx := &gin.Context{}
|
||||
for _, s := range sessions {
|
||||
if s.SessionType == model.SESSIONTYPE_WEB {
|
||||
doOfflineOnlineSession(ctx, s.SessionId, "")
|
||||
continue
|
||||
}
|
||||
s.Monitors = &sync.Map{}
|
||||
onlineSession.LoadOrStore(s.SessionId, s)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpsertSession godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param sessino body model.Session true "session"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /session [post]
|
||||
func (c *Controller) UpsertSession(ctx *gin.Context) {
|
||||
data := &model.Session{}
|
||||
if err := ctx.BindJSON(data); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if err := mysql.DB.
|
||||
Clauses(clause.OnConflict{
|
||||
DoUpdates: clause.AssignmentColumns([]string{"status", "closed_at"}),
|
||||
}).
|
||||
Create(data).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Status {
|
||||
case model.SESSIONSTATUS_ONLINE:
|
||||
if data.Monitors == nil {
|
||||
data.Monitors = &sync.Map{}
|
||||
}
|
||||
_, ok := onlineSession.LoadOrStore(data.SessionId, data)
|
||||
if ok {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": "failed to loadstore online session"}})
|
||||
return
|
||||
}
|
||||
case model.SESSIONSTATUS_OFFLINE:
|
||||
// doOfflineOnlineSession(ctx, data.SessionId, "")
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
||||
|
||||
// CreateSessionCommand godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param sessioncmd body model.SessionCmd true "SessionCmd"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /session/cmd [post]
|
||||
func (c *Controller) CreateSessionCmd(ctx *gin.Context) {
|
||||
data := &model.SessionCmd{}
|
||||
if err := ctx.BindJSON(data); err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
if err := mysql.DB.
|
||||
Create(data).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
||||
|
||||
// GetSessions godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param page_index query int true "page_index"
|
||||
// @Param page_size query int true "page_size"
|
||||
// @Param search query string false "search"
|
||||
// @Param status query int false "status, online=1, offline=2"
|
||||
// @Param start query string false "start, RFC3339"
|
||||
// @Param end query string false "end, RFC3339"
|
||||
// @Param uid query int false "uid"
|
||||
// @Param asset_id query int false "asset id"
|
||||
// @Param client_ip query string false "client_ip"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Session}}
|
||||
// @Router /session [get]
|
||||
func (c *Controller) GetSessions(ctx *gin.Context) {
|
||||
db := mysql.DB.Model(&model.Session{})
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
if !acl.IsAdmin(currentUser) {
|
||||
db = db.Where("uid = ?", currentUser.Uid)
|
||||
}
|
||||
db = filterSearch(ctx, db, "user_name", "asset_info", "gateway_info", "account_info")
|
||||
db, err := filterStartEnd(ctx, db)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db = filterEqual(ctx, db, "status", "uid", "asset_id", "client_ip")
|
||||
|
||||
doGet[*model.Session](ctx, false, db, "", sessionPostHooks...)
|
||||
}
|
||||
|
||||
// GetSessionCmds godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param page_index query int true "page_index"
|
||||
// @Param page_size query int true "page_size"
|
||||
// @Param session_id path string true "session id"
|
||||
// @Param search query string true "search"
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.SessionCmd}}
|
||||
// @Router /session/:session_id/cmd [get]
|
||||
func (c *Controller) GetSessionCmds(ctx *gin.Context) {
|
||||
db := mysql.DB.Model(&model.SessionCmd{})
|
||||
db = db.Where("session_id = ?", ctx.Param("session_id"))
|
||||
db = filterSearch(ctx, db, "cmd", "result")
|
||||
|
||||
doGet[*model.SessionCmd](ctx, false, db, "")
|
||||
}
|
||||
|
||||
// GetSessionOptionAsset godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.SessionOptionAsset}}
|
||||
// @Router /session/option/asset [get]
|
||||
func (c *Controller) GetSessionOptionAsset(ctx *gin.Context) {
|
||||
opts := make([]*model.SessionOptionAsset, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Select("id, name").
|
||||
Find(&opts).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(opts))
|
||||
}
|
||||
|
||||
// GetSessionOptionClientIp godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Success 200 {object} HttpResponse{data=[]string}
|
||||
// @Router /session/option/clientip [get]
|
||||
func (c *Controller) GetSessionOptionClientIp(ctx *gin.Context) {
|
||||
opts := make([]string, 0)
|
||||
if err := mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Distinct("client_ip").
|
||||
Find(&opts).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(opts))
|
||||
}
|
||||
|
||||
// CreateSessionReplay godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param session_id path string true "session id"
|
||||
// @Success 200 {object} HttpResponse
|
||||
// @Router /session/replay/:session_id [post]
|
||||
func (c *Controller) CreateSessionReplay(ctx *gin.Context) {
|
||||
file, _, err := ctx.Request.FormFile("replay.cast")
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join("/replay", fmt.Sprintf("%s.cast", ctx.Param("session_id"))))
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.Write(content)
|
||||
|
||||
ctx.JSON(http.StatusOK, defaultHttpResponse)
|
||||
}
|
||||
|
||||
// GetSessionReplay godoc
|
||||
//
|
||||
// @Tags session
|
||||
// @Param session_id path string true "session id"
|
||||
// @Success 200 {object} string
|
||||
// @Router /session/replay/:session_id [get]
|
||||
func (c *Controller) GetSessionReplay(ctx *gin.Context) {
|
||||
sessionId := ctx.Param("session_id")
|
||||
filename := fmt.Sprintf("%s.cast", sessionId)
|
||||
ctx.FileAttachment(filepath.Join("/replay", filename), filename)
|
||||
}
|
300
backend/pkg/server/controller/stat.go
Normal file
300
backend/pkg/server/controller/stat.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
"github.com/veops/oneterm/pkg/server/model"
|
||||
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
|
||||
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
|
||||
)
|
||||
|
||||
// StatAssetType godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAssetType}}
|
||||
// @Router /stat/assettype [get]
|
||||
func (c *Controller) StatAssetType(ctx *gin.Context) {
|
||||
stat := make([]*model.StatAssetType, 0)
|
||||
key := "stat-assettype"
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
return
|
||||
}
|
||||
|
||||
t := mysql.DB.
|
||||
Model(&model.Asset{}).
|
||||
Raw(`
|
||||
WITH RECURSIVE cte AS(
|
||||
SELECT parent_id
|
||||
FROM asset
|
||||
WHERE deleted_at = 0
|
||||
UNION ALL
|
||||
SELECT t.parent_id
|
||||
FROM cte
|
||||
INNER JOIN node t on cte.parent_id = t.id
|
||||
WHERE deleted_at = 0
|
||||
)
|
||||
SELECT
|
||||
parent_id,
|
||||
COUNT(*) AS count
|
||||
FROM cte
|
||||
GROUP BY parent_id
|
||||
`)
|
||||
|
||||
err := mysql.DB.
|
||||
Model(&model.Node{}).
|
||||
Select("node.name, t.count").
|
||||
Joins("LEFT JOIN (?) t ON node.id = t.parent_id", t).
|
||||
Where("node.parent_id = 0").
|
||||
Find(&stat).
|
||||
Error
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
}
|
||||
|
||||
// StatCount godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Success 200 {object} HttpResponse{data=model.StatCount}
|
||||
// @Router /stat/count [get]
|
||||
func (c *Controller) StatCount(ctx *gin.Context) {
|
||||
stat := &model.StatCount{}
|
||||
key := "stat-count"
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
|
||||
return
|
||||
}
|
||||
|
||||
eg := &errgroup.Group{}
|
||||
eg.Go(func() error {
|
||||
return mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Select("COUNT(DISTINCT asset_id, account_id) as connect, COUNT(DISTINCT uid) as user, COUNT(DISTINCT gateway_id) as gateway, COUNT(*) as session").
|
||||
Where("status = 1").
|
||||
First(&stat).
|
||||
Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return mysql.DB.Model(&model.Asset{}).Count(&stat.TotalAsset).Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return mysql.DB.Model(&model.Asset{}).Where("connectable = 1").Count(&stat.Asset).Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
return mysql.DB.Model(&model.Gateway{}).Count(&stat.TotalGateway).Error
|
||||
})
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
stat.Gateway = lo.Ternary(stat.Gateway <= stat.TotalGateway, stat.Gateway, stat.TotalGateway)
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
|
||||
}
|
||||
|
||||
// StatAccount godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Param type query string true "account name" Enums(day, week, month)
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAccount}}
|
||||
// @Router /stat/account [get]
|
||||
func (c *Controller) StatAccount(ctx *gin.Context) {
|
||||
start, end := time.Now(), time.Now()
|
||||
switch ctx.Query("type") {
|
||||
case "day":
|
||||
start = start.Add(-time.Hour * 24)
|
||||
case "week":
|
||||
start = start.Add(-time.Hour * 24 * 7)
|
||||
case "month":
|
||||
start = start.Add(-time.Hour * 24 * 30)
|
||||
default:
|
||||
ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("wrong time range %s", ctx.Query("type")))
|
||||
return
|
||||
}
|
||||
|
||||
stat := make([]*model.StatAccount, 0)
|
||||
key := "stat-account-" + ctx.Query("type")
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
return
|
||||
}
|
||||
|
||||
err := mysql.DB.
|
||||
Model(&model.Account{}).
|
||||
Select("account.name, COUNT(*) AS count").
|
||||
Joins("LEFT JOIN session ON account.id = session.account_id").
|
||||
Group("account.id").
|
||||
Order("count DESC").
|
||||
Limit(10).
|
||||
Where("session.created_at >= ? AND session.created_at <= ?", start, end).
|
||||
Find(&stat).
|
||||
Error
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
}
|
||||
|
||||
// StatAsset godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Param type query string true "account name" Enums(day, week, month)
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAsset}}
|
||||
// @Router /stat/asset [get]
|
||||
func (c *Controller) StatAsset(ctx *gin.Context) {
|
||||
start, end := time.Now(), time.Now()
|
||||
interval := time.Hour * 24
|
||||
dateFmt := "%Y-%m-%d"
|
||||
timeFmt := time.DateOnly
|
||||
switch ctx.Query("type") {
|
||||
case "day":
|
||||
start = start.Add(-time.Hour * 24)
|
||||
interval = time.Hour
|
||||
dateFmt = "%Y-%m-%d %H:00:00"
|
||||
timeFmt = time.DateTime
|
||||
case "week":
|
||||
start = start.Add(-time.Hour * 24 * 7)
|
||||
case "month":
|
||||
start = start.Add(-time.Hour * 24 * 30)
|
||||
default:
|
||||
ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("wrong time range %s", ctx.Query("type")))
|
||||
return
|
||||
}
|
||||
|
||||
stat := make([]*model.StatAsset, 0)
|
||||
key := "stat-asset-" + ctx.Query("type")
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
return
|
||||
}
|
||||
err := mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Select("COUNT(DISTINCT asset_id, uid) AS connect, COUNT(*) AS session, COUNT(DISTINCT asset_id) AS asset, COUNT(DISTINCT uid) AS user, DATE_FORMAT(created_at, ?) AS time", dateFmt).
|
||||
Where("session.created_at >= ? AND session.created_at <= ?", start, end).
|
||||
Group("time").
|
||||
Find(&stat).
|
||||
Error
|
||||
if err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
for ; !start.After(end); start = start.Add(interval) {
|
||||
t := start.Truncate(interval).Format(timeFmt)
|
||||
if lo.ContainsBy(stat, func(s *model.StatAsset) bool { return t == s.Time }) {
|
||||
continue
|
||||
}
|
||||
stat = append(stat, &model.StatAsset{Time: t})
|
||||
}
|
||||
|
||||
sort.Slice(stat, func(i, j int) bool { return stat[i].Time < stat[j].Time })
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
}
|
||||
|
||||
// StatCountOfUser godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Success 200 {object} HttpResponse{data=model.StatCountOfUser}
|
||||
// @Router /stat/count/ofuser [get]
|
||||
func (c *Controller) StatCountOfUser(ctx *gin.Context) {
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
stat := &model.StatCountOfUser{}
|
||||
key := fmt.Sprintf("stat-count-%d-", currentUser.Uid)
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
|
||||
return
|
||||
}
|
||||
|
||||
eg := &errgroup.Group{}
|
||||
eg.Go(func() error {
|
||||
return mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Select("COUNT(DISTINCT asset_id, account_id) as connect, COUNT(DISTINCT asset_id) as asset, COUNT(*) as session").
|
||||
Where("status = 1").
|
||||
Where("uid = ?", currentUser.Uid).
|
||||
First(&stat).
|
||||
Error
|
||||
})
|
||||
eg.Go(func() error {
|
||||
isAdmin := acl.IsAdmin(currentUser)
|
||||
db := mysql.DB.Model(&model.Asset{})
|
||||
if !isAdmin {
|
||||
authorizationResourceIds, err := GetAutorizationResourceIds(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db = db.Where("id IN (?)", mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("resource_id IN ?", authorizationResourceIds))
|
||||
}
|
||||
return db.Count(&stat.TotalAsset).Error
|
||||
})
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
|
||||
}
|
||||
|
||||
// StatRankOfUser godoc
|
||||
//
|
||||
// @Tags stat
|
||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAsset}}
|
||||
// @Router /stat/rank/ofuser [get]
|
||||
func (c *Controller) StatRankOfUser(ctx *gin.Context) {
|
||||
stat := make([]*model.StatRankOfUser, 0)
|
||||
key := "stat-rank-user"
|
||||
if redis.Get(ctx, key, stat) == nil {
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := mysql.DB.
|
||||
Model(&model.Session{}).
|
||||
Select("uid, COUNT(*) AS count, MAX(created_at) AS last_time").
|
||||
Group("uid").
|
||||
Order("count DESC").
|
||||
Limit(3).
|
||||
Find(&stat).
|
||||
Error; err != nil {
|
||||
ctx.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
redis.SetEx(ctx, key, stat, time.Minute)
|
||||
|
||||
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
|
||||
}
|
||||
|
||||
func toListData[T any](data []T) *ListData {
|
||||
return &ListData{
|
||||
Count: int64(len(data)),
|
||||
List: lo.Map(data, func(d T, _ int) any { return d }),
|
||||
}
|
||||
}
|
56
backend/pkg/server/model/account.go
Normal file
56
backend/pkg/server/model/account.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
AccountType int `json:"account_type" gorm:"column:account_type"`
|
||||
Account string `json:"account" gorm:"column:account"`
|
||||
Password string `json:"password" gorm:"column:password"`
|
||||
Pk string `json:"pk" gorm:"column:pk"`
|
||||
Phrase string `json:"phrase" gorm:"column:phrase"`
|
||||
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
|
||||
AssetCount int64 `json:"asset_count" gorm:"-"`
|
||||
}
|
||||
|
||||
func (m *Account) TableName() string {
|
||||
return "account"
|
||||
}
|
||||
func (m *Account) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *Account) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *Account) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *Account) SetResourceId(resourceId int) {
|
||||
m.ResourceId = resourceId
|
||||
}
|
||||
func (m *Account) GetResourceId() int {
|
||||
return m.ResourceId
|
||||
}
|
||||
func (m *Account) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *Account) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type AccountCount struct {
|
||||
Id int `json:"id" gorm:"id"`
|
||||
Count int64 `json:"count" gorm:"count"`
|
||||
}
|
72
backend/pkg/server/model/asset.go
Normal file
72
backend/pkg/server/model/asset.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Asset struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Ciid int `json:"ci_id" gorm:"column:ci_id"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
||||
Ip string `json:"ip" gorm:"column:ip"`
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
|
||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||
NodeChain string `json:"node_chain" gorm:"-"`
|
||||
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
type AccessAuth struct {
|
||||
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
|
||||
End *time.Time `json:"end,omitempty" gorm:"column:end"`
|
||||
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids"`
|
||||
Ranges Slice[Range] `json:"ranges" gorm:"column:ranges"`
|
||||
Allow bool `json:"allow" gorm:"column:allow"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
Week int `json:"week" gorm:"column:week"`
|
||||
Times Slice[string] `json:"times" gorm:"column:times"`
|
||||
}
|
||||
|
||||
func (m *Asset) TableName() string {
|
||||
return "asset"
|
||||
}
|
||||
func (m *Asset) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *Asset) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *Asset) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *Asset) SetResourceId(resourceId int) {
|
||||
m.ResourceId = resourceId
|
||||
}
|
||||
func (m *Asset) GetResourceId() int {
|
||||
return m.ResourceId
|
||||
}
|
||||
func (m *Asset) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *Asset) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type AssetNodeChain struct {
|
||||
NodeId int `gorm:"column:id"`
|
||||
Chain string `gorm:"column:chain"`
|
||||
}
|
86
backend/pkg/server/model/authorization.go
Normal file
86
backend/pkg/server/model/authorization.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Authorization struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
AssetId int `json:"asset_id" gorm:"column:asset_id"`
|
||||
AccountId int `json:"account_id" gorm:"column:account_id"`
|
||||
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *Authorization) TableName() string {
|
||||
return "authorization"
|
||||
}
|
||||
|
||||
type InfoModel interface {
|
||||
GetId() int
|
||||
}
|
||||
|
||||
type AssetInfo struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
||||
Ip string `json:"ip" gorm:"column:ip"`
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
||||
Connectable bool `json:"connectable" gorm:"column:connectable"`
|
||||
NodeChain string `json:"node_chain" gorm:"-"`
|
||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||
Authorization Map[int, Slice[int]] `json:"-" gorm:"column:authorization"`
|
||||
GatewayId int `json:"-" gorm:"column:gateway_id"`
|
||||
Gateway *GatewayInfo `json:"gateway,omitempty" gorm:"-"`
|
||||
Accounts []*AccountInfo `json:"accounts" gorm:"-"`
|
||||
Commands []*CmdInfo `json:"commands" gorm:"-"`
|
||||
}
|
||||
|
||||
func (m *AssetInfo) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type AccountInfo struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Account string `json:"account" gorm:"column:account"`
|
||||
AccountType int `json:"account_type,omitempty" gorm:"column:account_type"`
|
||||
Password string `json:"password,omitempty" gorm:"column:password"`
|
||||
}
|
||||
|
||||
func (m *AccountInfo) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type GatewayInfo struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Host string `json:"host" gorm:"column:host"`
|
||||
Port int `json:"port" gorm:"column:port"`
|
||||
AccountType int `json:"account_type" gorm:"column:account_type"`
|
||||
Account string `json:"account" gorm:"column:account"`
|
||||
Password string `json:"password" gorm:"column:password"`
|
||||
}
|
||||
|
||||
func (m *GatewayInfo) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type CmdInfo struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Cmds Slice[string] `json:"cmds" gorm:"column:cmds"`
|
||||
Enable int `json:"enable" gorm:"column:enable"`
|
||||
}
|
||||
|
||||
func (m *CmdInfo) GetId() int {
|
||||
return m.Id
|
||||
}
|
46
backend/pkg/server/model/command.go
Normal file
46
backend/pkg/server/model/command.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Cmds Slice[string] `json:"cmds" gorm:"column:cmds"`
|
||||
Enable bool `json:"enable" gorm:"column:enable"`
|
||||
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *Command) TableName() string {
|
||||
return "command"
|
||||
}
|
||||
func (m *Command) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *Command) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *Command) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *Command) SetResourceId(resourceId int) {
|
||||
m.ResourceId = resourceId
|
||||
}
|
||||
func (m *Command) GetResourceId() int {
|
||||
return m.ResourceId
|
||||
}
|
||||
func (m *Command) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *Command) GetId() int {
|
||||
return m.Id
|
||||
}
|
22
backend/pkg/server/model/config.go
Normal file
22
backend/pkg/server/model/config.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Timeout int `json:"timeout" gorm:"column:timeout"`
|
||||
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *Config) TableName() string {
|
||||
return "config"
|
||||
}
|
58
backend/pkg/server/model/gateway.go
Normal file
58
backend/pkg/server/model/gateway.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Host string `json:"host" gorm:"column:host"`
|
||||
Port int `json:"port" gorm:"column:port"`
|
||||
AccountType int `json:"account_type" gorm:"column:account_type"`
|
||||
Account string `json:"account" gorm:"column:account"`
|
||||
Password string `json:"password" gorm:"column:password"`
|
||||
Pk string `json:"pk" gorm:"column:pk"`
|
||||
Phrase string `json:"phrase" gorm:"column:phrase"`
|
||||
|
||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
|
||||
AssetCount int64 `json:"asset_count" gorm:"-"`
|
||||
}
|
||||
|
||||
func (m *Gateway) TableName() string {
|
||||
return "gateway"
|
||||
}
|
||||
func (m *Gateway) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *Gateway) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *Gateway) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *Gateway) SetResourceId(resourceId int) {
|
||||
m.ResourceId = resourceId
|
||||
}
|
||||
func (m *Gateway) GetResourceId() int {
|
||||
return m.ResourceId
|
||||
}
|
||||
func (m *Gateway) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *Gateway) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type GatewayCount struct {
|
||||
Id int `gorm:"column:id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
22
backend/pkg/server/model/history.go
Normal file
22
backend/pkg/server/model/history.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type History struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
RemoteIp string `json:"remote_ip" gorm:"column:remote_ip"`
|
||||
Type string `json:"type" gorm:"column:type"`
|
||||
TargetId int `json:"target_id" gorm:"column:target_id"`
|
||||
ActionType int `json:"action_type" gorm:"column:action_type"`
|
||||
Old Map[string, any] `json:"old" gorm:"column:old"`
|
||||
New Map[string, any] `json:"new" gorm:"column:new"`
|
||||
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (m *History) TableName() string {
|
||||
return "history"
|
||||
}
|
44
backend/pkg/server/model/model.go
Normal file
44
backend/pkg/server/model/model.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
ACTION_CREATE = iota + 1
|
||||
ACTION_DELETE
|
||||
ACTION_UPDATE
|
||||
)
|
||||
|
||||
type Slice[T int | string | Range] []T
|
||||
|
||||
func (s *Slice[T]) Scan(value any) error {
|
||||
return json.Unmarshal(value.([]byte), s)
|
||||
}
|
||||
|
||||
func (s Slice[T]) Value() (driver.Value, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
type Map[K comparable, V any] map[K]V
|
||||
|
||||
func (m *Map[K, V]) Scan(value any) error {
|
||||
return json.Unmarshal(value.([]byte), m)
|
||||
|
||||
}
|
||||
|
||||
func (m Map[K, V]) Value() (driver.Value, error) {
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
type Model interface {
|
||||
TableName() string
|
||||
SetId(int)
|
||||
SetCreatorId(int)
|
||||
SetUpdaterId(int)
|
||||
SetResourceId(int)
|
||||
GetResourceId() int
|
||||
GetId() int
|
||||
GetName() string
|
||||
}
|
67
backend/pkg/server/model/node.go
Normal file
67
backend/pkg/server/model/node.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
||||
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
|
||||
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
|
||||
*Sync `json:"sync" gorm:"column:sync"`
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
|
||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||
|
||||
// ResourceId int `json:"resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
|
||||
AssetCount int64 `json:"asset_count" gorm:"-"`
|
||||
HasChild bool `json:"has_child" gorm:"-"`
|
||||
}
|
||||
|
||||
type Sync struct {
|
||||
TypeId int `json:"type_id,omitempty" gorm:"column:type_id"`
|
||||
Mapping Map[string, string] `json:"mapping" gorm:"column:mapping"`
|
||||
Filters string `json:"filters" gorm:"column:filters"`
|
||||
Enable bool `json:"enable" gorm:"column:enable"`
|
||||
Frequency float64 `json:"frequency" gorm:"column:frequency"`
|
||||
}
|
||||
|
||||
func (m *Node) TableName() string {
|
||||
return "node"
|
||||
}
|
||||
func (m *Node) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *Node) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *Node) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *Node) SetResourceId(resourceId int) {
|
||||
|
||||
}
|
||||
func (m *Node) GetResourceId() int {
|
||||
return 0
|
||||
}
|
||||
func (m *Node) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *Node) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type NodeCount struct {
|
||||
ParentId int `gorm:"column:parent_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
69
backend/pkg/server/model/publicKey.go
Normal file
69
backend/pkg/server/model/publicKey.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
const (
|
||||
AUTHMETHOD_PASSWORD = 1
|
||||
AUTHMETHOD_PUBLICKEY = 2
|
||||
)
|
||||
|
||||
type PublicKey struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Uid int `json:"uid" gorm:"column:uid"`
|
||||
UserName string `json:"username" gorm:"column:username"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Mac string `json:"mac" gorm:"column:mac"`
|
||||
Pk string `json:"pk" gorm:"column:pk"`
|
||||
|
||||
// ResourceId int `json:"resource_id"`
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *PublicKey) TableName() string {
|
||||
return "public_key"
|
||||
}
|
||||
func (m *PublicKey) SetId(id int) {
|
||||
m.Id = id
|
||||
}
|
||||
func (m *PublicKey) SetCreatorId(creatorId int) {
|
||||
m.CreatorId = creatorId
|
||||
}
|
||||
func (m *PublicKey) SetUpdaterId(updaterId int) {
|
||||
m.UpdaterId = updaterId
|
||||
}
|
||||
func (m *PublicKey) SetResourceId(resourceId int) {
|
||||
|
||||
}
|
||||
func (m *PublicKey) GetResourceId() int {
|
||||
return 0
|
||||
}
|
||||
func (m *PublicKey) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
func (m *PublicKey) GetId() int {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
type ReqAuth struct {
|
||||
Method int `json:"method"`
|
||||
UserName string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Pk string `json:"pk"`
|
||||
}
|
||||
|
||||
type UserInfoResp struct {
|
||||
Result UserInfoRespResult `json:"result"`
|
||||
}
|
||||
|
||||
type UserInfoRespResult struct {
|
||||
Uid int `json:"uid"`
|
||||
Rid int `json:"rid"`
|
||||
}
|
115
backend/pkg/server/model/session.go
Normal file
115
backend/pkg/server/model/session.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
SESSIONTYPE_WEB = iota + 1
|
||||
SESSIONTYPE_CLIENT
|
||||
)
|
||||
|
||||
const (
|
||||
SESSIONSTATUS_ONLINE = iota + 1
|
||||
SESSIONSTATUS_OFFLINE
|
||||
)
|
||||
|
||||
const (
|
||||
SESSIONACTION_NEW = iota + 1
|
||||
SESSIONACTION_MONITOR
|
||||
SESSIONACTION_CLOSE
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
SessionType int `json:"session_type" gorm:"column:session_type"`
|
||||
SessionId string `json:"session_id" gorm:"column:session_id"`
|
||||
Uid int `json:"uid" gorm:"column:uid"`
|
||||
UserName string `json:"user_name" gorm:"column:user_name"`
|
||||
AssetId int `json:"asset_id" gorm:"column:asset_id"`
|
||||
AssetInfo string `json:"asset_info" gorm:"column:asset_info"`
|
||||
AccountId int `json:"account_id" gorm:"column:account_id"`
|
||||
AccountInfo string `json:"account_info" gorm:"column:account_info"`
|
||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||
GatewayInfo string `json:"gateway_info" gorm:"column:gateway_info"`
|
||||
ClientIp string `json:"client_ip" gorm:"column:client_ip"`
|
||||
Protocol string `json:"protocol" gorm:"column:protocol"`
|
||||
Status int `json:"status" gorm:"column:status"`
|
||||
Duration int64 `json:"duration" gorm:"-"`
|
||||
ClosedAt *time.Time `json:"closed_at" gorm:"column:closed_at"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
|
||||
CmdCount int64 `json:"cmd_count" gorm:"-"`
|
||||
|
||||
Monitors *sync.Map `json:"-" gorm:"-"`
|
||||
Chans *SessionChans `json:"-" gorm:"-"`
|
||||
Connected atomic.Bool `json:"-" gorm:"-"`
|
||||
}
|
||||
|
||||
func (m *Session) TableName() string {
|
||||
return "session"
|
||||
}
|
||||
|
||||
type SessionCmd struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
SessionId string `json:"session_id" gorm:"column:session_id"`
|
||||
Cmd string `json:"cmd" gorm:"column:cmd"`
|
||||
Result string `json:"result" gorm:"column:result"`
|
||||
Level int `json:"level" gorm:"column:level"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (m *SessionCmd) TableName() string {
|
||||
return "session_cmd"
|
||||
}
|
||||
|
||||
type CmdCount struct {
|
||||
SessionId string `gorm:"column:session_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
|
||||
type SessionOptionAsset struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey"`
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
}
|
||||
|
||||
type SshReq struct {
|
||||
Uid int `json:"uid"`
|
||||
UserName string `json:"username"`
|
||||
Cookie string `json:"cookie"`
|
||||
AcceptLanguage string `json:"accept_language"`
|
||||
ClientIp string `json:"client_ip"`
|
||||
AssetId int `json:"asset_id"`
|
||||
AccountId int `json:"account_id"`
|
||||
Protocol string `json:"protocol"`
|
||||
Action int `json:"action"`
|
||||
SessionId string `json:"session_id"`
|
||||
}
|
||||
|
||||
type SshResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
SessionId string `json:"session_id"`
|
||||
Uid int `json:"uid"`
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
type SessionChans struct {
|
||||
Rin io.Reader
|
||||
Win io.Writer
|
||||
ErrChan chan error
|
||||
RespChan chan *SshResp
|
||||
InChan chan []byte
|
||||
OutChan chan []byte
|
||||
Buf *bytes.Buffer
|
||||
WindowChan chan string
|
||||
AwayChan chan struct{}
|
||||
CloseChan chan string
|
||||
}
|
47
backend/pkg/server/model/stat.go
Normal file
47
backend/pkg/server/model/stat.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type StatAssetType struct {
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Count int `json:"count" gorm:"column:count"`
|
||||
}
|
||||
|
||||
type StatCount struct {
|
||||
Connect int64 `json:"connect" gorm:"column:connect"`
|
||||
Session int64 `json:"session" gorm:"column:session"`
|
||||
Asset int64 `json:"asset" gorm:"column:asset"`
|
||||
TotalAsset int64 `json:"total_asset" gorm:"column:total_asset"`
|
||||
User int64 `json:"user" gorm:"column:user"`
|
||||
// TotalUser int64 `json:"total_user"`
|
||||
Gateway int64 `json:"gateway" gorm:"column:gateway"`
|
||||
TotalGateway int64 `json:"total_gateway" gorm:"column:total_gateway"`
|
||||
}
|
||||
|
||||
type StatAccount struct {
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Count int `json:"count" gorm:"column:count"`
|
||||
}
|
||||
|
||||
type StatAsset struct {
|
||||
Connect int64 `json:"connect" gorm:"column:connect"`
|
||||
Session int64 `json:"session" gorm:"column:session"`
|
||||
Asset int64 `json:"asset" gorm:"column:asset"`
|
||||
User int64 `json:"user" gorm:"column:user"`
|
||||
Time string `json:"time" gorm:"column:time"`
|
||||
}
|
||||
|
||||
type StatCountOfUser struct {
|
||||
Connect int64 `json:"connect" gorm:"column:connect"`
|
||||
Session int64 `json:"session" gorm:"column:session"`
|
||||
Asset int64 `json:"asset" gorm:"column:asset"`
|
||||
TotalAsset int64 `json:"total_asset" gorm:"column:total_asset"`
|
||||
}
|
||||
|
||||
type StatRankOfUser struct {
|
||||
Uid int `json:"uid" gorm:"column:uid"`
|
||||
Count int64 `json:"count" gorm:"column:count"`
|
||||
LastTime time.Time `json:"last_time" gorm:"column:last_time"`
|
||||
}
|
89
backend/pkg/server/remote/http.go
Normal file
89
backend/pkg/server/remote/http.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/spf13/cast"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
|
||||
)
|
||||
|
||||
var (
|
||||
RC = resty.NewWithClient(&http.Client{}).SetRetryCount(3)
|
||||
)
|
||||
|
||||
func GetAclToken(ctx context.Context) (res string, err error) {
|
||||
res, err = redis.RC.Get(ctx, "aclToken").Result()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
aclConfig := conf.Cfg.Auth.Acl
|
||||
|
||||
url := fmt.Sprintf("%s%s", aclConfig.Url, "/acl/apps/token")
|
||||
secretHash := md5.Sum([]byte(aclConfig.SecretKey))
|
||||
secretKey := hex.EncodeToString(secretHash[:])
|
||||
|
||||
data := make(map[string]string)
|
||||
resp, err := RC.R().
|
||||
SetBody(map[string]any{"app_id": aclConfig.AppId, "secret_key": secretKey}).
|
||||
SetResult(&data).
|
||||
Post(url)
|
||||
if err = HandleErr(err, resp, func(dt map[string]any) bool { return dt["token"] != "" }); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = data["token"]
|
||||
_, err = redis.RC.SetNX(ctx, "aclToken", res, time.Hour).Result()
|
||||
return
|
||||
}
|
||||
|
||||
func HandleErr(e error, resp *resty.Response, isOk func(dt map[string]any) bool) (err error) {
|
||||
pc, _, _, _ := runtime.Caller(1)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
bs, _ := json.Marshal(resp.Request.Body)
|
||||
logger.L.Error(fmt.Sprintf("%s failed", runtime.FuncForPC(pc).Name()), zap.String("url", resp.Request.URL), zap.String("req", string(bs)), zap.String("resp", resp.String()))
|
||||
}
|
||||
}()
|
||||
|
||||
err = e
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dt := make(map[string]any)
|
||||
err = json.Unmarshal(resp.Body(), &dt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != 200 || (isOk != nil && !isOk(dt)) {
|
||||
err = &RemoteError{
|
||||
HttpCode: resp.StatusCode(),
|
||||
Resp: dt,
|
||||
}
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RemoteError struct {
|
||||
HttpCode int
|
||||
Resp map[string]any
|
||||
}
|
||||
|
||||
func (r *RemoteError) Error() string {
|
||||
return cast.ToString(r.Resp["message"])
|
||||
}
|
98
backend/pkg/server/router/middleware/auth.go
Normal file
98
backend/pkg/server/router/middleware/auth.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Package middleware
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/auth/acl"
|
||||
)
|
||||
|
||||
var (
|
||||
basicAuthDb = sync.Map{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
basicAuthDb.Store("admin", "admin")
|
||||
}
|
||||
|
||||
func Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
if conf.Cfg.Auth.Acl != nil && conf.Cfg.Auth.Acl.Url != "" {
|
||||
err, ok = authAcl(c)
|
||||
} else {
|
||||
// TODO: add your auth here
|
||||
ok = true
|
||||
}
|
||||
if !ok {
|
||||
logger.L.Warn(err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "authorized refused",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AuthToken() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.GetHeader("X-Token") != conf.Cfg.SshServer.Xtoken {
|
||||
logger.L.Warn("invalid token", zap.String("X-Token", c.GetHeader("X-Token")))
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"message": "authorized refused",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func authAcl(ctx *gin.Context) (error, bool) {
|
||||
session := &acl.Session{}
|
||||
|
||||
sess, err := ctx.Cookie("session")
|
||||
if err == nil && sess != "" {
|
||||
s := acl.NewSignature(conf.Cfg.SecretKey, "cookie-session", "", "hmac", nil, nil)
|
||||
content, err := s.Unsign(sess)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &session)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
|
||||
ctx.Set("session", session)
|
||||
return nil, true
|
||||
}
|
||||
return fmt.Errorf("no session"), false
|
||||
}
|
||||
|
||||
//func authBasic(ctx *gin.Context) (error, bool) {
|
||||
// if user, password, ok := ctx.Request.BasicAuth(); ok {
|
||||
// if p, ok := basicAuthDb.Load(user); ok && p.(string) == password {
|
||||
// return nil, true
|
||||
// } else {
|
||||
// return fmt.Errorf("invalid user or password"), false
|
||||
// }
|
||||
// }
|
||||
// return fmt.Errorf("invalid user or password"), false
|
||||
//}
|
||||
|
||||
//func authWithWhiteList(ip string) bool {
|
||||
// return lo.Contains(viper.GetStringSlice("gateway.whiteList"), ip)
|
||||
//}
|
151
backend/pkg/server/router/middleware/cache.go
Normal file
151
backend/pkg/server/router/middleware/cache.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
|
||||
)
|
||||
|
||||
var (
|
||||
cachePrefix = "App::HttpCache"
|
||||
)
|
||||
|
||||
type responseCache struct {
|
||||
Status int
|
||||
Header http.Header
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (c *responseCache) fillWithCacheWriter(cacheWriter *responseCacheWriter) {
|
||||
c.Status = cacheWriter.Status()
|
||||
c.Data = cacheWriter.body.Bytes()
|
||||
c.Header = cacheWriter.Header().Clone()
|
||||
}
|
||||
|
||||
// responseCacheWriter
|
||||
type responseCacheWriter struct {
|
||||
gin.ResponseWriter
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *responseCacheWriter) Write(b []byte) (int, error) {
|
||||
w.body.Write(b)
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func replyWithCache(c *gin.Context, respCache *responseCache) {
|
||||
c.Writer.WriteHeader(respCache.Status)
|
||||
|
||||
for key, values := range respCache.Header {
|
||||
for _, val := range values {
|
||||
c.Writer.Header().Set(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.Writer.Write(respCache.Data); err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func RCache(ctx context.Context, defaultExpire time.Duration) gin.HandlerFunc {
|
||||
sfGroup := singleflight.Group{}
|
||||
return func(c *gin.Context) {
|
||||
|
||||
cacheKey := fmt.Sprintf("%s::%s", cachePrefix, c.Request.RequestURI)
|
||||
cacheDuration := defaultExpire
|
||||
|
||||
// read cache first
|
||||
{
|
||||
respCache := &responseCache{}
|
||||
err := RGet(ctx, cacheKey, &respCache)
|
||||
if err == nil {
|
||||
replyWithCache(c, respCache)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cacheWriter := &responseCacheWriter{ResponseWriter: c.Writer}
|
||||
c.Writer = cacheWriter
|
||||
|
||||
inFlight := false
|
||||
rawRespCache, _, _ := sfGroup.Do(cacheKey, func() (any, error) {
|
||||
forgetTimer := time.AfterFunc(time.Second*15, func() {
|
||||
sfGroup.Forget(cacheKey)
|
||||
})
|
||||
defer forgetTimer.Stop()
|
||||
|
||||
c.Next()
|
||||
|
||||
inFlight = true
|
||||
respCache := &responseCache{}
|
||||
respCache.fillWithCacheWriter(cacheWriter)
|
||||
// only cache 2xx response
|
||||
if !c.IsAborted() && cacheWriter.Status() < 300 && cacheWriter.Status() >= 200 {
|
||||
if err := RSet(ctx, cacheKey, respCache, cacheDuration); err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
}
|
||||
return respCache, nil
|
||||
})
|
||||
|
||||
if !inFlight {
|
||||
replyWithCache(c, rawRespCache.(*responseCache))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// redis
|
||||
|
||||
func RSet(ctx context.Context, key string, value any, expire time.Duration) error {
|
||||
payload, err := Serialize(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = redis.RC.SetEx(ctx, key, payload, expire).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func RDelete(ctx context.Context, key string) error {
|
||||
_, err := redis.RC.Del(ctx, key).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func RGet(ctx context.Context, key string, value any) error {
|
||||
r, err := redis.RC.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return Deserialize(r, value)
|
||||
}
|
||||
|
||||
// codec
|
||||
|
||||
func Serialize(value any) ([]byte, error) {
|
||||
var b bytes.Buffer
|
||||
encoder := gob.NewEncoder(&b)
|
||||
if err := encoder.Encode(value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func Deserialize(byt []byte, ptr any) (err error) {
|
||||
b := bytes.NewBuffer(byt)
|
||||
decoder := gob.NewDecoder(b)
|
||||
if err = decoder.Decode(ptr); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
63
backend/pkg/server/router/middleware/error.go
Normal file
63
backend/pkg/server/router/middleware/error.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/server/controller"
|
||||
)
|
||||
|
||||
type bodyWriter struct {
|
||||
gin.ResponseWriter
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
func (w bodyWriter) Write(b []byte) (int, error) {
|
||||
return w.body.Write(b)
|
||||
}
|
||||
|
||||
func Error2Resp() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if strings.Contains(ctx.Request.URL.String(), "session/replay") {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
wb := &bodyWriter{
|
||||
body: &bytes.Buffer{},
|
||||
ResponseWriter: ctx.Writer,
|
||||
}
|
||||
ctx.Writer = wb
|
||||
|
||||
ctx.Next()
|
||||
|
||||
obj := make(map[string]any)
|
||||
json.Unmarshal(wb.body.Bytes(), &obj)
|
||||
if len(ctx.Errors) > 0 {
|
||||
if v, ok := obj["code"]; !ok || v == 0 {
|
||||
obj["code"] = ctx.Writer.Status()
|
||||
}
|
||||
|
||||
if v, ok := obj["message"]; !ok || v == "" {
|
||||
e := ctx.Errors.Last().Err
|
||||
obj["message"] = e.Error()
|
||||
|
||||
ae, ok := e.(*controller.ApiError)
|
||||
if ok {
|
||||
lang := ctx.PostForm("lang")
|
||||
accept := ctx.GetHeader("Accept-Language")
|
||||
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
|
||||
obj["message"] = ae.Message(localizer)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
bs, _ := json.Marshal(obj)
|
||||
wb.ResponseWriter.Write(bs)
|
||||
}
|
||||
}
|
137
backend/pkg/server/router/middleware/log.go
Normal file
137
backend/pkg/server/router/middleware/log.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Package middleware
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
handler "github.com/veops/oneterm/pkg/server/controller"
|
||||
)
|
||||
|
||||
var (
|
||||
NotLogUrls = []string{"/favicon.ico"}
|
||||
)
|
||||
|
||||
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
query := c.Request.URL.RawQuery
|
||||
|
||||
c.Next()
|
||||
|
||||
if !lo.Contains(NotLogUrls, path) {
|
||||
cost := time.Since(start)
|
||||
logger.Info(path,
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", path),
|
||||
zap.String("query", query),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
zap.String("user-agent", c.Request.UserAgent()),
|
||||
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
|
||||
zap.Duration("cost", cost),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
var brokenPipe bool
|
||||
if ne, ok := err.(*net.OpError); ok {
|
||||
if se, ok := ne.Err.(*os.SyscallError); ok {
|
||||
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
|
||||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
|
||||
brokenPipe = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
if brokenPipe {
|
||||
logger.Error(c.Request.URL.Path,
|
||||
zap.Any("error", err),
|
||||
zap.String("request", string(httpRequest)),
|
||||
)
|
||||
err := c.Error(err.(error))
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if stack {
|
||||
logger.Error("[Recovery from panic]",
|
||||
zap.Any("error", err),
|
||||
zap.String("request", string(httpRequest)),
|
||||
zap.String("stack", string(debug.Stack())),
|
||||
)
|
||||
} else {
|
||||
logger.Error("[Recovery from panic]",
|
||||
zap.Any("error", err),
|
||||
zap.String("request", string(httpRequest)),
|
||||
)
|
||||
}
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// LogRequest LogUpdate Record the specified HTTP request (including the URL and data) in a log or another location.
|
||||
func LogRequest() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !lo.Contains([]string{"GET", "HEAD"}, c.Request.Method) {
|
||||
|
||||
data := map[string]any{}
|
||||
bodyBytes, _ := io.ReadAll(c.Request.Body)
|
||||
_ = c.Request.Body.Close()
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if err := json.Unmarshal(bodyBytes, &data); err != nil {
|
||||
if valid, err := permissionCheck(c, data); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest,
|
||||
handler.HttpResponse{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
} else if !valid {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest,
|
||||
handler.HttpResponse{Code: http.StatusForbidden, Message: "no permission"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
excludeUrls := map[string]struct{}{}
|
||||
if _, ok := excludeUrls[c.Request.RequestURI]; !ok {
|
||||
if c.Request.Method != "POST" {
|
||||
w := &responseWriter{ResponseWriter: c.Writer, body: []byte{}}
|
||||
c.Writer = w
|
||||
logger.L.Info("request record", zap.String("body", string(w.body)))
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func permissionCheck(ctx *gin.Context, data map[string]any) (bool, error) {
|
||||
return true, nil
|
||||
}
|
62
backend/pkg/server/router/middleware/middleware.go
Normal file
62
backend/pkg/server/router/middleware/middleware.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
)
|
||||
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
method := c.Request.Method
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
if origin != "" {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token, session")
|
||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||
c.Header("Access-Control-Max-Age", "172800")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
if method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.L.Sugar().Errorf("Panic info is: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RecoveryWithWriter() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
var buf [4096]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
logger.L.Error(string(buf[:n]))
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
gin.ResponseWriter
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(data []byte) (int, error) {
|
||||
w.body = append(w.body, data...)
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
119
backend/pkg/server/router/router.go
Normal file
119
backend/pkg/server/router/router.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/viper"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
"github.com/veops/oneterm/docs"
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/router/middleware"
|
||||
"github.com/veops/oneterm/pkg/util"
|
||||
)
|
||||
|
||||
var routeGroup []*GroupRoute
|
||||
|
||||
func Server(cfg *conf.ConfigYaml) *http.Server {
|
||||
routeConfig()
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", cfg.Http.Host, cfg.Http.Port),
|
||||
Handler: setupRouter(),
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.L.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
logger.L.Info(fmt.Sprintf("start on server:%s", srv.Addr))
|
||||
return srv
|
||||
}
|
||||
|
||||
func GracefulExit(srv *http.Server, ch chan struct{}) {
|
||||
<-ch
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
logger.L.Error(err.Error())
|
||||
}
|
||||
logger.L.Info("Shutdown server ...")
|
||||
}
|
||||
|
||||
func routeConfig() {
|
||||
var commonRoute []Route
|
||||
commonRoute = append(commonRoute, routes...)
|
||||
routeGroup = []*GroupRoute{
|
||||
{
|
||||
Prefix: "/api/oneterm/v1",
|
||||
GroupMiddleware: gin.HandlersChain{
|
||||
middleware.Error2Resp(),
|
||||
middleware.RecoveryWithWriter(),
|
||||
},
|
||||
SubRoutes: commonRoute,
|
||||
},
|
||||
{
|
||||
Prefix: "",
|
||||
SubRoutes: baseRoutes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setupRouter() *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(
|
||||
middleware.GinLogger(logger.L),
|
||||
middleware.LogRequest(),
|
||||
middleware.Cors(),
|
||||
middleware.GinRecovery(logger.L, true))
|
||||
// sso
|
||||
gob.Register(map[string]any{}) // important!
|
||||
store := cookie.NewStore([]byte(viper.GetString("gateway.secretKey")))
|
||||
r.Use(sessions.Sessions("session", store))
|
||||
|
||||
routeGroupsMap := make(map[string]*gin.RouterGroup)
|
||||
for _, gRoute := range routeGroup {
|
||||
if _, ok := routeGroupsMap[gRoute.Prefix]; !ok {
|
||||
routeGroupsMap[gRoute.Prefix] = r.Group(gRoute.Prefix)
|
||||
}
|
||||
for _, gMiddleware := range gRoute.GroupMiddleware {
|
||||
routeGroupsMap[gRoute.Prefix].Use(gMiddleware)
|
||||
}
|
||||
|
||||
for _, subRoute := range gRoute.SubRoutes {
|
||||
length := len(subRoute.Middleware) + 2
|
||||
routes := make([]any, length)
|
||||
routes[0] = subRoute.Pattern
|
||||
for i, v := range subRoute.Middleware {
|
||||
routes[i+1] = v
|
||||
}
|
||||
routes[length-1] = subRoute.HandlerFunc
|
||||
|
||||
util.CallReflect(
|
||||
routeGroupsMap[gRoute.Prefix],
|
||||
subRoute.Method,
|
||||
routes...)
|
||||
}
|
||||
}
|
||||
r.Handle("GET", "/metrics", gin.WrapH(promhttp.Handler()))
|
||||
// swagger
|
||||
docs.SwaggerInfo.Title = "ONETERM API"
|
||||
docs.SwaggerInfo.BasePath = "/api/oneterm/v1"
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
return r
|
||||
}
|
411
backend/pkg/server/router/routers.go
Normal file
411
backend/pkg/server/router/routers.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
"github.com/veops/oneterm/pkg/server/controller"
|
||||
"github.com/veops/oneterm/pkg/server/router/middleware"
|
||||
)
|
||||
|
||||
var (
|
||||
c = controller.NewController()
|
||||
|
||||
baseRoutes = []Route{
|
||||
{
|
||||
Name: "a health check, just for monitoring",
|
||||
Method: "GET",
|
||||
Pattern: "/-/health",
|
||||
HandlerFunc: func(ctx *gin.Context) {
|
||||
ctx.String(http.StatusOK, "OK")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "favicon.ico",
|
||||
Method: "GET",
|
||||
Pattern: "/favicon.ico",
|
||||
HandlerFunc: func(ctx *gin.Context) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "change the log level",
|
||||
Method: "PUT",
|
||||
Pattern: "/-/log/level",
|
||||
HandlerFunc: func(ctx *gin.Context) {
|
||||
logger.AtomicLevel.ServeHTTP(ctx.Writer, ctx.Request)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
routes = []Route{
|
||||
// account
|
||||
{
|
||||
Name: "create a account",
|
||||
Method: "POST",
|
||||
Pattern: "account",
|
||||
HandlerFunc: c.CreateAccount,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a account",
|
||||
Method: "DELETE",
|
||||
Pattern: "account/:id",
|
||||
HandlerFunc: c.DeleteAccount,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a account",
|
||||
Method: "PUT",
|
||||
Pattern: "account/:id",
|
||||
HandlerFunc: c.UpdateAccount,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query accounts",
|
||||
Method: "GET",
|
||||
Pattern: "account",
|
||||
HandlerFunc: c.GetAccounts,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
// asset
|
||||
{
|
||||
Name: "create a asset",
|
||||
Method: "POST",
|
||||
Pattern: "asset",
|
||||
HandlerFunc: c.CreateAsset,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a asset",
|
||||
Method: "DELETE",
|
||||
Pattern: "asset/:id",
|
||||
HandlerFunc: c.DeleteAsset,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a asset",
|
||||
Method: "PUT",
|
||||
Pattern: "asset/:id",
|
||||
HandlerFunc: c.UpdateAsset,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query assets",
|
||||
Method: "GET",
|
||||
Pattern: "asset",
|
||||
HandlerFunc: c.GetAssets,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update asset by server",
|
||||
Method: "PUT",
|
||||
Pattern: "asset/update_by_server",
|
||||
HandlerFunc: c.UpdateByServer,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
{
|
||||
Name: "query asset by server",
|
||||
Method: "GET",
|
||||
Pattern: "asset/query_by_server",
|
||||
HandlerFunc: c.QueryByServer,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
// command
|
||||
{
|
||||
Name: "create a command",
|
||||
Method: "POST",
|
||||
Pattern: "command",
|
||||
HandlerFunc: c.CreateCommand,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a command",
|
||||
Method: "DELETE",
|
||||
Pattern: "command/:id",
|
||||
HandlerFunc: c.DeleteCommand,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a command",
|
||||
Method: "PUT",
|
||||
Pattern: "command/:id",
|
||||
HandlerFunc: c.UpdateCommand,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query commands",
|
||||
Method: "GET",
|
||||
Pattern: "command",
|
||||
HandlerFunc: c.GetCommands,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "modify config",
|
||||
Method: "POST",
|
||||
Pattern: "config",
|
||||
HandlerFunc: c.PostConfig,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query config",
|
||||
Method: "GET",
|
||||
Pattern: "config",
|
||||
HandlerFunc: c.GetConfig,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
// gateway
|
||||
{
|
||||
Name: "create a gateway",
|
||||
Method: "POST",
|
||||
Pattern: "gateway",
|
||||
HandlerFunc: c.CreateGateway,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a gateway",
|
||||
Method: "DELETE",
|
||||
Pattern: "gateway/:id",
|
||||
HandlerFunc: c.DeleteGateway,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a gateway",
|
||||
Method: "PUT",
|
||||
Pattern: "gateway/:id",
|
||||
HandlerFunc: c.UpdateGateway,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query gateways",
|
||||
Method: "GET",
|
||||
Pattern: "gateway",
|
||||
HandlerFunc: c.GetGateways,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
// node
|
||||
{
|
||||
Name: "create a node",
|
||||
Method: "POST",
|
||||
Pattern: "node",
|
||||
HandlerFunc: c.CreateNode,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a node",
|
||||
Method: "DELETE",
|
||||
Pattern: "node/:id",
|
||||
HandlerFunc: c.DeleteNode,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a node",
|
||||
Method: "PUT",
|
||||
Pattern: "node/:id",
|
||||
HandlerFunc: c.UpdateNode,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query nodes",
|
||||
Method: "GET",
|
||||
Pattern: "node",
|
||||
HandlerFunc: c.GetNodes,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
// publicKey
|
||||
{
|
||||
Name: "create a publicKey",
|
||||
Method: "POST",
|
||||
Pattern: "public_key",
|
||||
HandlerFunc: c.CreatePublicKey,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "delete a publicKey",
|
||||
Method: "DELETE",
|
||||
Pattern: "public_key/:id",
|
||||
HandlerFunc: c.DeletePublicKey,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "update a publicKey",
|
||||
Method: "PUT",
|
||||
Pattern: "public_key/:id",
|
||||
HandlerFunc: c.UpdatePublicKey,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query publicKeys",
|
||||
Method: "GET",
|
||||
Pattern: "public_key",
|
||||
HandlerFunc: c.GetPublicKeys,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "auth by publicKey or password",
|
||||
Method: "POST",
|
||||
Pattern: "public_key/auth",
|
||||
HandlerFunc: c.Auth,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
//stat
|
||||
{
|
||||
Name: "query stat asset type",
|
||||
Method: "GET",
|
||||
Pattern: "stat/assettype",
|
||||
HandlerFunc: c.StatAssetType,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query stat count",
|
||||
Method: "GET",
|
||||
Pattern: "stat/count",
|
||||
HandlerFunc: c.StatCount,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query stat count of user",
|
||||
Method: "GET",
|
||||
Pattern: "stat/count/ofuser",
|
||||
HandlerFunc: c.StatCountOfUser,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query stat account",
|
||||
Method: "GET",
|
||||
Pattern: "stat/account",
|
||||
HandlerFunc: c.StatAccount,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query stat asset",
|
||||
Method: "GET",
|
||||
Pattern: "stat/asset",
|
||||
HandlerFunc: c.StatAsset,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query stat rank of user",
|
||||
Method: "GET",
|
||||
Pattern: "stat/rank/ofuser",
|
||||
HandlerFunc: c.StatRankOfUser,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
//session
|
||||
{
|
||||
Name: "query session",
|
||||
Method: "GET",
|
||||
Pattern: "session",
|
||||
HandlerFunc: c.GetSessions,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query session cmds",
|
||||
Method: "GET",
|
||||
Pattern: "session/:session_id/cmd",
|
||||
HandlerFunc: c.GetSessionCmds,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query session option asset",
|
||||
Method: "GET",
|
||||
Pattern: "session/option/asset",
|
||||
HandlerFunc: c.GetSessionOptionAsset,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query session option client ip",
|
||||
Method: "GET",
|
||||
Pattern: "session/option/clientip",
|
||||
HandlerFunc: c.GetSessionOptionClientIp,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query session replay",
|
||||
Method: "GET",
|
||||
Pattern: "session/replay/:session_id",
|
||||
HandlerFunc: c.GetSessionReplay,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "create sesssin replay",
|
||||
Method: "POST",
|
||||
Pattern: "session/replay/:session_id",
|
||||
HandlerFunc: c.CreateSessionReplay,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
{
|
||||
Name: "upsert session",
|
||||
Method: "POST",
|
||||
Pattern: "session",
|
||||
HandlerFunc: c.UpsertSession,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
{
|
||||
Name: "create sesssin cmd",
|
||||
Method: "POST",
|
||||
Pattern: "session/cmd",
|
||||
HandlerFunc: c.CreateSessionCmd,
|
||||
Middleware: gin.HandlersChain{middleware.AuthToken()},
|
||||
},
|
||||
//history
|
||||
{
|
||||
Name: "query history",
|
||||
Method: "GET",
|
||||
Pattern: "history",
|
||||
HandlerFunc: c.GetHistories,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "query history type mapping",
|
||||
Method: "GET",
|
||||
Pattern: "history/type/mapping",
|
||||
HandlerFunc: c.GetHistoryTypeMapping,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
//connect
|
||||
{
|
||||
Name: "connect",
|
||||
Method: "POST",
|
||||
Pattern: "connect/:asset_id/:account_id/:protocol",
|
||||
HandlerFunc: c.Connect,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "connecting",
|
||||
Method: "GET",
|
||||
Pattern: "connect/:session_id",
|
||||
HandlerFunc: c.Connecting,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "connect monitor",
|
||||
Method: "GET",
|
||||
Pattern: "connect/monitor/:session_id",
|
||||
HandlerFunc: c.ConnectMonitor,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
{
|
||||
Name: "connect close",
|
||||
Method: "POST",
|
||||
Pattern: "connect/close/:session_id",
|
||||
HandlerFunc: c.ConnectClose,
|
||||
Middleware: gin.HandlersChain{middleware.Auth()},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Name string
|
||||
Method string
|
||||
Pattern string
|
||||
HandlerFunc func(ctx *gin.Context)
|
||||
Middleware []gin.HandlerFunc
|
||||
}
|
||||
|
||||
type GroupRoute struct {
|
||||
Prefix string
|
||||
GroupMiddleware gin.HandlersChain
|
||||
SubRoutes []Route
|
||||
}
|
21
backend/pkg/server/storage/cache/local/localcache.go
vendored
Normal file
21
backend/pkg/server/storage/cache/local/localcache.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/allegro/bigcache/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// LC local cache client
|
||||
LC *bigcache.BigCache
|
||||
)
|
||||
|
||||
func Init() error {
|
||||
var err error
|
||||
if LC, err = bigcache.New(context.Background(), bigcache.DefaultConfig(time.Minute*10)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
56
backend/pkg/server/storage/cache/redis/redis.go
vendored
Normal file
56
backend/pkg/server/storage/cache/redis/redis.go
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
)
|
||||
|
||||
var (
|
||||
// RC redis cache client
|
||||
RC *redis.Client
|
||||
)
|
||||
|
||||
func Init(cfg *conf.RedisConfig) (err error) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
readTimeout := time.Duration(30) * time.Second
|
||||
writeTimeout := time.Duration(30) * time.Second
|
||||
RC = redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
DB: cfg.Db,
|
||||
Password: cfg.Password,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MaxIdleConns: cfg.MaxIdle,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
})
|
||||
|
||||
if _, err = RC.Ping(ctx).Result(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Get(ctx context.Context, key string, dst any) (err error) {
|
||||
bs, err := RC.Get(ctx, key).Bytes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.Unmarshal(bs, dst)
|
||||
}
|
||||
|
||||
func SetEx(ctx context.Context, key string, src any, exp time.Duration) (err error) {
|
||||
bs, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return RC.SetEx(ctx, key, bs, exp).Err()
|
||||
}
|
27
backend/pkg/server/storage/db/mysql/mysql.go
Normal file
27
backend/pkg/server/storage/db/mysql/mysql.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/veops/oneterm/pkg/conf"
|
||||
)
|
||||
|
||||
var (
|
||||
DB *gorm.DB
|
||||
)
|
||||
|
||||
func Init(cfg *conf.MysqlConfig) (err error) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/oneterm?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Ip, cfg.Port)
|
||||
if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user