fix: 修复关闭SSH终端标签页时会话状态未更新的问题

This commit is contained in:
2026-04-18 02:35:38 +08:00
commit 6e2e2f9387
43467 changed files with 5489040 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
package service
import (
"context"
"errors"
"next-terminal/server/common"
"next-terminal/server/common/nt"
"next-terminal/server/dto"
"next-terminal/server/env"
"next-terminal/server/global/cache"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
var AccessTokenService = new(accessTokenService)
type accessTokenService struct {
baseService
}
func (service accessTokenService) GenAccessToken(userId string, tokenType string) (*model.AccessToken, error) {
var accessToken *model.AccessToken
err := env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx := service.Context(tx)
user, err := repository.UserRepository.FindById(ctx, userId)
if err != nil {
return err
}
token := "forever-" + utils.UUID()
accessToken = &model.AccessToken{
ID: utils.UUID(),
UserId: userId,
Token: token,
Type: tokenType,
Created: common.NowJsonTime(),
}
authorization := dto.Authorization{
Token: token,
Remember: false,
Type: nt.AccessToken,
User: &user,
}
cache.TokenManager.Set(token, authorization, cache.NoExpiration)
return repository.AccessTokenRepository.Create(ctx, accessToken)
})
return accessToken, err
}
func (service accessTokenService) Reload() error {
accessTokens, err := repository.AccessTokenRepository.FindAll(context.TODO())
if err != nil {
return err
}
for _, accessToken := range accessTokens {
user, err := repository.UserRepository.FindById(context.TODO(), accessToken.UserId)
if err != nil {
return err
}
authorization := dto.Authorization{
Token: accessToken.Token,
Remember: false,
Type: nt.AccessToken,
User: &user,
}
cache.TokenManager.Set(accessToken.Token, authorization, cache.NoExpiration)
}
return nil
}
func (service accessTokenService) DelAccessToken(ctx context.Context, id string, userId string) error {
oldAccessTokens, err := repository.AccessTokenRepository.FindByUserId(ctx, userId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
for _, t := range oldAccessTokens {
if t.Token != "" {
cache.TokenManager.Delete(t.Token)
}
}
return repository.AccessTokenRepository.DeleteById(ctx, id)
}
func (service accessTokenService) DelAccessTokenByUserId(ctx context.Context, userId string) error {
oldAccessTokens, err := repository.AccessTokenRepository.FindByUserId(ctx, userId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
for _, t := range oldAccessTokens {
if t.Token != "" {
cache.TokenManager.Delete(t.Token)
}
}
return repository.AccessTokenRepository.DeleteByUserId(ctx, userId)
}
+307
View File
@@ -0,0 +1,307 @@
package service
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"golang.org/x/net/proxy"
"net"
"next-terminal/server/common/maps"
"next-terminal/server/common/nt"
"strconv"
"strings"
"time"
"next-terminal/server/common"
"next-terminal/server/config"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
var AssetService = new(assetService)
type assetService struct {
baseService
}
func (s assetService) EncryptAll() error {
items, err := repository.AssetRepository.FindAll(context.TODO())
if err != nil {
return err
}
for i := range items {
item := items[i]
if item.Encrypted {
continue
}
if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
if err := repository.AssetRepository.UpdateById(context.TODO(), &item, item.ID); err != nil {
return err
}
}
return nil
}
func (s assetService) Decrypt(item *model.Asset, password []byte) error {
if item.Encrypted {
if item.Password != "" && item.Password != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Password)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.Password = string(decryptedCBC)
}
if item.PrivateKey != "" && item.PrivateKey != "-" {
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.PrivateKey = string(decryptedCBC)
}
if item.Passphrase != "" && item.Passphrase != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.Passphrase = string(decryptedCBC)
}
}
return nil
}
func (s assetService) Encrypt(item *model.Asset, password []byte) error {
if item.Password != "" && item.Password != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password)
if err != nil {
return err
}
item.Password = base64.StdEncoding.EncodeToString(encryptedCBC)
}
if item.PrivateKey != "" && item.PrivateKey != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password)
if err != nil {
return err
}
item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC)
}
if item.Passphrase != "" && item.Passphrase != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password)
if err != nil {
return err
}
item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC)
}
item.Encrypted = true
return nil
}
func (s assetService) FindByIdAndDecrypt(c context.Context, id string) (model.Asset, error) {
asset, err := repository.AssetRepository.FindById(c, id)
if err != nil {
return model.Asset{}, err
}
if err := s.Decrypt(&asset, config.GlobalCfg.EncryptionPassword); err != nil {
return model.Asset{}, err
}
return asset, nil
}
func (s assetService) CheckStatus(asset *model.Asset, ip string, port int) (bool, error) {
attributes, err := repository.AssetRepository.FindAssetAttrMapByAssetId(context.Background(), asset.ID)
if err != nil {
return false, err
}
if "true" == attributes[nt.SocksProxyEnable] {
socks5 := fmt.Sprintf("%s:%s", attributes[nt.SocksProxyHost], attributes[nt.SocksProxyPort])
auth := &proxy.Auth{
User: attributes[nt.SocksProxyUsername],
Password: attributes[nt.SocksProxyPassword],
}
dailer, err := proxy.SOCKS5("tcp", socks5, auth, &net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 15 * time.Second,
})
if err != nil {
return false, err
}
target := fmt.Sprintf("%s:%s", ip, strconv.Itoa(port))
c, err := dailer.Dial("tcp", target)
if err != nil {
return false, err
}
defer c.Close()
return true, nil
} else {
accessGatewayId := asset.AccessGatewayId
if accessGatewayId != "" && accessGatewayId != "-" {
g, err := GatewayService.GetGatewayById(accessGatewayId)
if err != nil {
return false, err
}
uuid := utils.UUID()
defer g.CloseSshTunnel(uuid)
exposedIP, exposedPort, err := g.OpenSshTunnel(uuid, ip, port)
if err != nil {
return false, err
}
return utils.Tcping(exposedIP, exposedPort)
}
return utils.Tcping(ip, port)
}
}
func (s assetService) Create(ctx context.Context, m maps.Map) (*model.Asset, error) {
if tags, ok := m["tags"]; ok {
switch v := tags.(type) {
case []string:
m["tags"] = strings.Join(v, ",")
case []interface{}:
parts := make([]string, len(v))
for i, item := range v {
if s, ok := item.(string); ok {
parts[i] = s
}
}
m["tags"] = strings.Join(parts, ",")
}
}
data, err := json.Marshal(m)
if err != nil {
return nil, err
}
var item model.Asset
if err := json.Unmarshal(data, &item); err != nil {
return nil, err
}
item.ID = utils.UUID()
item.Created = common.NowJsonTime()
item.Active = true
return &item, s.Transaction(ctx, func(ctx context.Context) error {
if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
if err := repository.AssetRepository.Create(ctx, &item); err != nil {
return err
}
if err := repository.AssetRepository.UpdateAttributes(ctx, item.ID, item.Protocol, m); err != nil {
return err
}
return nil
})
}
func (s assetService) DeleteById(id string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := s.Context(tx)
// 删除资产
if err := repository.AssetRepository.DeleteById(c, id); err != nil {
return err
}
// 删除资产属性
if err := repository.AssetRepository.DeleteAttrByAssetId(c, id); err != nil {
return err
}
// 删除资产与用户的关系
if err := repository.AuthorisedRepository.DeleteByAssetId(c, id); err != nil {
return err
}
return nil
})
}
func (s assetService) UpdateById(id string, m maps.Map) error {
data, err := json.Marshal(m)
if err != nil {
return err
}
var item model.Asset
if err := json.Unmarshal(data, &item); err != nil {
return err
}
switch item.AccountType {
case "credential":
item.Username = "-"
item.Password = "-"
item.PrivateKey = "-"
item.Passphrase = "-"
case "private-key":
item.Password = "-"
item.CredentialId = "-"
if len(item.Username) == 0 {
item.Username = "-"
}
if len(item.Passphrase) == 0 {
item.Passphrase = "-"
}
case "custom":
item.PrivateKey = "-"
item.Passphrase = "-"
item.CredentialId = "-"
if len(item.Username) == 0 {
item.Username = "-"
}
if len(item.Password) == 0 {
item.Password = "-"
}
}
if len(item.Tags) == 0 {
item.Tags = "-"
}
if item.Description == "" {
item.Description = "-"
}
if item.AccessGatewayId == "" {
item.AccessGatewayId = "-"
}
if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
return s.Transaction(context.Background(), func(ctx context.Context) error {
if err := repository.AssetRepository.UpdateById(ctx, &item, id); err != nil {
return err
}
if err := repository.AssetRepository.UpdateAttributes(ctx, id, item.Protocol, m); err != nil {
return err
}
return nil
})
}
func (s assetService) FixSshMode() error {
return repository.AssetRepository.UpdateAttrs(context.TODO(), "ssh-mode", "naive", nt.Native)
}
+130
View File
@@ -0,0 +1,130 @@
package service
import (
"context"
"next-terminal/server/common"
"next-terminal/server/dto"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"github.com/pkg/errors"
"gorm.io/gorm"
)
var AuthorisedService = new(authorisedService)
type authorisedService struct {
baseService
}
func (s authorisedService) AuthorisedAssets(ctx context.Context, item *dto.AuthorisedAsset) error {
return s.Transaction(ctx, func(ctx context.Context) error {
var items []model.Authorised
for _, assetId := range item.AssetIds {
id := utils.Sign([]string{assetId, item.UserId, item.UserGroupId})
if err := repository.AuthorisedRepository.DeleteById(ctx, id); err != nil {
return err
}
authorised := model.Authorised{
ID: id,
AssetId: assetId,
CommandFilterId: item.CommandFilterId,
StrategyId: item.StrategyId,
UserId: item.UserId,
UserGroupId: item.UserGroupId,
Created: common.NowJsonTime(),
}
items = append(items, authorised)
}
return repository.AuthorisedRepository.CreateInBatches(ctx, items)
})
}
func (s authorisedService) AuthorisedUsers(ctx context.Context, item *dto.AuthorisedUser) error {
return s.Transaction(ctx, func(ctx context.Context) error {
var items []model.Authorised
for _, userId := range item.UserIds {
id := utils.Sign([]string{item.AssetId, userId, ""})
if err := repository.AuthorisedRepository.DeleteById(ctx, id); err != nil {
return err
}
authorised := model.Authorised{
ID: id,
AssetId: item.AssetId,
CommandFilterId: item.CommandFilterId,
StrategyId: item.StrategyId,
UserId: userId,
UserGroupId: "",
Created: common.NowJsonTime(),
}
items = append(items, authorised)
}
return repository.AuthorisedRepository.CreateInBatches(ctx, items)
})
}
func (s authorisedService) AuthorisedUserGroups(ctx context.Context, item *dto.AuthorisedUserGroup) error {
return s.Transaction(ctx, func(ctx context.Context) error {
var items []model.Authorised
for _, userGroupId := range item.UserGroupIds {
id := utils.Sign([]string{item.AssetId, "", userGroupId})
if err := repository.AuthorisedRepository.DeleteById(ctx, id); err != nil {
return err
}
authorised := model.Authorised{
ID: id,
AssetId: item.AssetId,
CommandFilterId: item.CommandFilterId,
StrategyId: item.StrategyId,
UserId: "",
UserGroupId: userGroupId,
Created: common.NowJsonTime(),
}
items = append(items, authorised)
}
return repository.AuthorisedRepository.CreateInBatches(ctx, items)
})
}
func (s authorisedService) GetAuthorised(userId, assetId string) (item *model.Authorised, err error) {
id := utils.Sign([]string{assetId, userId, ""})
authorised, err := repository.AuthorisedRepository.FindById(context.Background(), id)
if err != nil {
if errors.Is(gorm.ErrRecordNotFound, err) {
groupIds, err := repository.UserGroupMemberRepository.FindUserGroupIdsByUserId(context.Background(), userId)
if err != nil {
return nil, err
}
for _, groupId := range groupIds {
id := utils.Sign([]string{assetId, "", groupId})
authorised, err := repository.AuthorisedRepository.FindById(context.Background(), id)
if err != nil {
continue
}
item = &authorised
break
}
return item, err
}
return nil, err
}
return &authorised, nil
}
+312
View File
@@ -0,0 +1,312 @@
package service
import (
"context"
"encoding/json"
"errors"
"strings"
"next-terminal/server/common"
"next-terminal/server/common/maps"
"next-terminal/server/common/nt"
"next-terminal/server/config"
"next-terminal/server/dto"
"next-terminal/server/env"
"next-terminal/server/global/security"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
var BackupService = new(backupService)
type backupService struct {
baseService
}
func (service backupService) Export() (error, *dto.Backup) {
ctx := context.TODO()
users, err := repository.UserRepository.FindAll(ctx)
if err != nil {
return err, nil
}
for i := range users {
users[i].Password = ""
}
userGroups, err := repository.UserGroupRepository.FindAll(ctx)
if err != nil {
return err, nil
}
if len(userGroups) > 0 {
for i := range userGroups {
members, err := repository.UserGroupMemberRepository.FindUserIdsByUserGroupId(ctx, userGroups[i].ID)
if err != nil {
return err, nil
}
userGroups[i].Members = members
}
}
storages, err := repository.StorageRepository.FindAll(ctx)
if err != nil {
return err, nil
}
strategies, err := repository.StrategyRepository.FindAll(ctx)
if err != nil {
return err, nil
}
jobs, err := repository.JobRepository.FindAll(ctx)
if err != nil {
return err, nil
}
accessSecurities, err := repository.SecurityRepository.FindAll(ctx)
if err != nil {
return err, nil
}
accessGateways, err := repository.GatewayRepository.FindAll(ctx)
if err != nil {
return err, nil
}
commands, err := repository.CommandRepository.FindAll(ctx)
if err != nil {
return err, nil
}
credentials, err := repository.CredentialRepository.FindAll(ctx)
if err != nil {
return err, nil
}
if len(credentials) > 0 {
for i := range credentials {
if err := CredentialService.Decrypt(&credentials[i], config.GlobalCfg.EncryptionPassword); err != nil {
return err, nil
}
}
}
assets, err := repository.AssetRepository.FindAll(ctx)
if err != nil {
return err, nil
}
var assetMaps = make([]map[string]interface{}, 0)
if len(assets) > 0 {
for i := range assets {
asset := assets[i]
if err := AssetService.Decrypt(&asset, config.GlobalCfg.EncryptionPassword); err != nil {
return err, nil
}
attributeMap, err := repository.AssetRepository.FindAssetAttrMapByAssetId(ctx, asset.ID)
if err != nil {
return err, nil
}
itemMap := utils.StructToMap(asset)
for key := range attributeMap {
itemMap[key] = attributeMap[key]
}
itemMap["created"] = asset.Created.Format("2006-01-02 15:04:05")
assetMaps = append(assetMaps, itemMap)
}
}
backup := dto.Backup{
Users: users,
UserGroups: userGroups,
Storages: storages,
Strategies: strategies,
Jobs: jobs,
AccessSecurities: accessSecurities,
AccessGateways: accessGateways,
Commands: commands,
Credentials: credentials,
Assets: assetMaps,
}
return nil, &backup
}
func (service backupService) Import(backup *dto.Backup) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx := service.Context(tx)
var userIdMapping = make(map[string]string)
if len(backup.Users) > 0 {
for _, item := range backup.Users {
oldId := item.ID
exist, err := repository.UserRepository.ExistByUsername(ctx, item.Username)
if err != nil {
return err
}
if exist {
delete(userIdMapping, oldId)
continue
}
newId := utils.UUID()
item.ID = newId
item.Password = utils.GenPassword()
if err := repository.UserRepository.Create(ctx, &item); err != nil {
return err
}
userIdMapping[oldId] = newId
}
}
var userGroupIdMapping = make(map[string]string)
if len(backup.UserGroups) > 0 {
for _, item := range backup.UserGroups {
oldId := item.ID
var members = make([]string, 0)
if len(item.Members) > 0 {
for _, member := range item.Members {
members = append(members, userIdMapping[member])
}
}
userGroup, err := UserGroupService.Create(ctx, item.Name, members)
if err != nil {
if errors.Is(nt.ErrNameAlreadyUsed, err) {
// 删除名称重复的用户组
delete(userGroupIdMapping, oldId)
continue
} else {
return err
}
}
userGroupIdMapping[oldId] = userGroup.ID
}
}
if len(backup.Storages) > 0 {
for _, item := range backup.Storages {
item.ID = utils.UUID()
item.Owner = userIdMapping[item.Owner]
item.Created = common.NowJsonTime()
if err := repository.StorageRepository.Create(ctx, &item); err != nil {
return err
}
}
}
var strategyIdMapping = make(map[string]string)
if len(backup.Strategies) > 0 {
for _, item := range backup.Strategies {
oldId := item.ID
newId := utils.UUID()
item.ID = newId
item.Created = common.NowJsonTime()
if err := repository.StrategyRepository.Create(ctx, &item); err != nil {
return err
}
strategyIdMapping[oldId] = newId
}
}
if len(backup.AccessSecurities) > 0 {
for _, item := range backup.AccessSecurities {
item.ID = utils.UUID()
if err := repository.SecurityRepository.Create(ctx, &item); err != nil {
return err
}
// 更新内存中的安全规则
rule := &security.Security{
ID: item.ID,
IP: item.IP,
Rule: item.Rule,
Priority: item.Priority,
}
security.GlobalSecurityManager.Add(rule)
}
}
var accessGatewayIdMapping = make(map[string]string)
if len(backup.AccessGateways) > 0 {
for _, item := range backup.AccessGateways {
oldId := item.ID
newId := utils.UUID()
item.ID = newId
item.Created = common.NowJsonTime()
if err := repository.GatewayRepository.Create(ctx, &item); err != nil {
return err
}
accessGatewayIdMapping[oldId] = newId
}
}
if len(backup.Commands) > 0 {
for _, item := range backup.Commands {
item.ID = utils.UUID()
item.Created = common.NowJsonTime()
if err := repository.CommandRepository.Create(ctx, &item); err != nil {
return err
}
}
}
var credentialIdMapping = make(map[string]string)
if len(backup.Credentials) > 0 {
for _, item := range backup.Credentials {
oldId := item.ID
newId := utils.UUID()
item.ID = newId
item.Owner = userIdMapping[item.Owner]
if err := CredentialService.Create(ctx, &item); err != nil {
return err
}
credentialIdMapping[oldId] = newId
}
}
var assetIdMapping = make(map[string]string)
if len(backup.Assets) > 0 {
for _, m := range backup.Assets {
data, err := json.Marshal(m)
if err != nil {
return err
}
m := maps.Map{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
credentialId := m["credentialId"].(string)
accessGatewayId := m["accessGatewayId"].(string)
if credentialId != "" && credentialId != "-" {
m["credentialId"] = credentialIdMapping[credentialId]
}
if accessGatewayId != "" && accessGatewayId != "-" {
m["accessGatewayId"] = accessGatewayIdMapping[accessGatewayId]
}
oldId := m["id"].(string)
m["owner"] = userIdMapping[m["owner"].(string)]
asset, err := AssetService.Create(ctx, m)
if err != nil {
return err
}
assetIdMapping[oldId] = asset.ID
}
}
if len(backup.Jobs) > 0 {
for _, item := range backup.Jobs {
if item.Func == nt.FuncCheckAssetStatusJob {
continue
}
item.ID = utils.UUID()
resourceIds := strings.Split(item.ResourceIds, ",")
if len(resourceIds) > 0 {
var newResourceIds = make([]string, 0)
for _, resourceId := range resourceIds {
newResourceIds = append(newResourceIds, assetIdMapping[resourceId])
}
item.ResourceIds = strings.Join(newResourceIds, ",")
}
if err := JobService.Create(ctx, &item); err != nil {
return err
}
}
}
return nil
})
}
+31
View File
@@ -0,0 +1,31 @@
package service
import (
"context"
"next-terminal/server/common/nt"
"next-terminal/server/env"
"gorm.io/gorm"
)
type baseService struct {
}
func (service baseService) Context(db *gorm.DB) context.Context {
return context.WithValue(context.TODO(), nt.DB, db)
}
func (service baseService) inTransaction(ctx context.Context) bool {
_, ok := ctx.Value(nt.DB).(*gorm.DB)
return ok
}
func (service baseService) Transaction(ctx context.Context, f func(ctx context.Context) error) error {
if !service.inTransaction(ctx) {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx := service.Context(tx)
return f(ctx)
})
}
return f(ctx)
}
+100
View File
@@ -0,0 +1,100 @@
package service
import (
"context"
"crypto/md5"
"fmt"
"next-terminal/server/common/nt"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
type Cli struct {
}
func NewCli() *Cli {
return &Cli{}
}
func (cli Cli) ResetPassword(username string) error {
user, err := repository.UserRepository.FindByUsername(context.TODO(), username)
if err != nil {
return err
}
password := "next-terminal"
passwd, err := utils.Encoder.Encode([]byte(password))
if err != nil {
return err
}
u := &model.User{
Password: string(passwd),
ID: user.ID,
}
if err := repository.UserRepository.Update(context.TODO(), u); err != nil {
return err
}
return nil
}
func (cli Cli) ResetTotp(username string) error {
user, err := repository.UserRepository.FindByUsername(context.TODO(), username)
if err != nil {
return err
}
u := &model.User{
TOTPSecret: "-",
ID: user.ID,
}
if err := repository.UserRepository.Update(context.TODO(), u); err != nil {
return err
}
return nil
}
func (cli Cli) ChangeEncryptionKey(oldEncryptionKey, newEncryptionKey string) error {
oldPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(oldEncryptionKey))))
newPassword := []byte(fmt.Sprintf("%x", md5.Sum([]byte(newEncryptionKey))))
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := context.WithValue(context.TODO(), nt.DB, tx)
credentials, err := repository.CredentialRepository.FindAll(c)
if err != nil {
return err
}
for i := range credentials {
credential := credentials[i]
if err := CredentialService.Decrypt(&credential, oldPassword); err != nil {
return err
}
if err := CredentialService.Encrypt(&credential, newPassword); err != nil {
return err
}
if err := repository.CredentialRepository.UpdateById(c, &credential, credential.ID); err != nil {
return err
}
}
assets, err := repository.AssetRepository.FindAll(c)
if err != nil {
return err
}
for i := range assets {
asset := assets[i]
if err := AssetService.Decrypt(&asset, oldPassword); err != nil {
return err
}
if err := AssetService.Encrypt(&asset, newPassword); err != nil {
return err
}
if err := repository.AssetRepository.UpdateById(c, &asset, asset.ID); err != nil {
return err
}
}
return nil
})
}
+120
View File
@@ -0,0 +1,120 @@
package service
import (
"context"
"encoding/base64"
"next-terminal/server/config"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var CredentialService = new(credentialService)
type credentialService struct {
}
func (s credentialService) EncryptAll() error {
items, err := repository.CredentialRepository.FindAll(context.TODO())
if err != nil {
return err
}
for i := range items {
item := items[i]
if item.Encrypted {
continue
}
if err := s.Encrypt(&item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
if err := repository.CredentialRepository.UpdateById(context.TODO(), &item, item.ID); err != nil {
return err
}
}
return nil
}
func (s credentialService) Encrypt(item *model.Credential, password []byte) error {
if item.Password != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Password), password)
if err != nil {
return err
}
item.Password = base64.StdEncoding.EncodeToString(encryptedCBC)
}
if item.PrivateKey != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.PrivateKey), password)
if err != nil {
return err
}
item.PrivateKey = base64.StdEncoding.EncodeToString(encryptedCBC)
}
if item.Passphrase != "-" {
encryptedCBC, err := utils.AesEncryptCBC([]byte(item.Passphrase), password)
if err != nil {
return err
}
item.Passphrase = base64.StdEncoding.EncodeToString(encryptedCBC)
}
item.Encrypted = true
return nil
}
func (s credentialService) Decrypt(item *model.Credential, password []byte) error {
if item.Encrypted {
if item.Password != "" && item.Password != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Password)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.Password = string(decryptedCBC)
}
if item.PrivateKey != "" && item.PrivateKey != "-" {
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.PrivateKey = string(decryptedCBC)
}
if item.Passphrase != "" && item.Passphrase != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, password)
if err != nil {
return err
}
item.Passphrase = string(decryptedCBC)
}
}
return nil
}
func (s credentialService) FindByIdAndDecrypt(ctx context.Context, id string) (o model.Credential, err error) {
credential, err := repository.CredentialRepository.FindById(ctx, id)
if err != nil {
return o, err
}
if err := s.Decrypt(&credential, config.GlobalCfg.EncryptionPassword); err != nil {
return o, err
}
return credential, nil
}
func (s credentialService) Create(ctx context.Context, item *model.Credential) error {
// 加密密码之后进行存储
if err := s.Encrypt(item, config.GlobalCfg.EncryptionPassword); err != nil {
return err
}
return repository.CredentialRepository.Create(ctx, item)
}
+48
View File
@@ -0,0 +1,48 @@
package service
import (
"context"
"next-terminal/server/global/gateway"
"next-terminal/server/model"
"next-terminal/server/repository"
)
var GatewayService = new(gatewayService)
type gatewayService struct{}
func (r gatewayService) GetGatewayById(accessGatewayId string) (g *gateway.Gateway, err error) {
g = gateway.GlobalGatewayManager.GetById(accessGatewayId)
if g == nil {
accessGateway, err := repository.GatewayRepository.FindById(context.TODO(), accessGatewayId)
if err != nil {
return nil, err
}
g = r.ReLoad(&accessGateway)
}
return g, nil
}
func (r gatewayService) LoadAll() error {
gateways, err := repository.GatewayRepository.FindAll(context.TODO())
if err != nil {
return err
}
if len(gateways) > 0 {
for i := range gateways {
r.ReLoad(&gateways[i])
}
}
return nil
}
func (r gatewayService) ReLoad(m *model.AccessGateway) *gateway.Gateway {
r.DisconnectById(m.ID)
g := gateway.GlobalGatewayManager.Add(m)
return g
}
func (r gatewayService) DisconnectById(id string) {
gateway.GlobalGatewayManager.Del(id)
}
+161
View File
@@ -0,0 +1,161 @@
package service
import (
"context"
"errors"
"next-terminal/server/common"
"next-terminal/server/common/nt"
"next-terminal/server/global/cron"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var JobService = new(jobService)
type jobService struct {
}
func (r jobService) ChangeStatusById(id, status string) error {
job, err := repository.JobRepository.FindById(context.TODO(), id)
if err != nil {
return err
}
if status == nt.JobStatusRunning {
j, err := getJob(&job)
if err != nil {
return err
}
entryID, err := cron.GlobalCron.AddJob(job.Cron, j)
if err != nil {
return err
}
log.Debug("开启计划任务", log.String("任务名称", job.Name), log.Int("运行中计划任务数量", len(cron.GlobalCron.Entries())))
jobForUpdate := model.Job{ID: id, Status: nt.JobStatusRunning, CronJobId: int(entryID)}
return repository.JobRepository.UpdateById(context.TODO(), &jobForUpdate)
} else {
cron.GlobalCron.Remove(cron.JobId(job.CronJobId))
log.Debug("关闭计划任务", log.String("任务名称", job.Name), log.Int("运行中计划任务数量", len(cron.GlobalCron.Entries())))
jobForUpdate := model.Job{ID: id, Status: nt.JobStatusNotRunning}
return repository.JobRepository.UpdateById(context.TODO(), &jobForUpdate)
}
}
func getJob(j *model.Job) (job cron.Job, err error) {
switch j.Func {
case nt.FuncCheckAssetStatusJob:
job = CheckAssetStatusJob{
ID: j.ID,
Mode: j.Mode,
ResourceIds: j.ResourceIds,
Metadata: j.Metadata,
}
case nt.FuncShellJob:
job = ShellJob{
ID: j.ID,
Mode: j.Mode,
ResourceIds: j.ResourceIds,
Metadata: j.Metadata,
}
default:
return nil, errors.New("未识别的任务")
}
return job, err
}
func (r jobService) ExecJobById(id string) (err error) {
job, err := repository.JobRepository.FindById(context.TODO(), id)
if err != nil {
return err
}
j, err := getJob(&job)
if err != nil {
return err
}
j.Run()
return nil
}
func (r jobService) InitJob() error {
jobs, _ := repository.JobRepository.FindAll(context.TODO())
if len(jobs) == 0 {
job := model.Job{
ID: utils.UUID(),
Name: "资产状态检测",
Func: nt.FuncCheckAssetStatusJob,
Cron: "0 0/10 * * * ?",
Mode: nt.JobModeAll,
Status: nt.JobStatusRunning,
Created: common.NowJsonTime(),
Updated: common.NowJsonTime(),
}
if err := repository.JobRepository.Create(context.TODO(), &job); err != nil {
return err
}
} else {
for i := range jobs {
if jobs[i].Status == nt.JobStatusRunning {
err := r.ChangeStatusById(jobs[i].ID, nt.JobStatusRunning)
if err != nil {
return err
}
}
}
}
return nil
}
func (r jobService) Create(ctx context.Context, o *model.Job) (err error) {
if o.Status == nt.JobStatusRunning {
j, err := getJob(o)
if err != nil {
return err
}
jobId, err := cron.GlobalCron.AddJob(o.Cron, j)
if err != nil {
return err
}
o.CronJobId = int(jobId)
}
return repository.JobRepository.Create(ctx, o)
}
func (r jobService) DeleteJobById(id string) error {
job, err := repository.JobRepository.FindById(context.TODO(), id)
if err != nil {
return err
}
if job.Status == nt.JobStatusRunning {
if err := r.ChangeStatusById(id, nt.JobStatusNotRunning); err != nil {
return err
}
}
return repository.JobRepository.DeleteJobById(context.TODO(), id)
}
func (r jobService) UpdateById(m *model.Job) error {
job, err := repository.JobRepository.FindById(context.TODO(), m.ID)
if err != nil {
return err
}
if err := repository.JobRepository.UpdateById(context.TODO(), m); err != nil {
return err
}
if job.Status == nt.JobStatusRunning {
if err := r.ChangeStatusById(m.ID, nt.JobStatusNotRunning); err != nil {
return err
}
if err := r.ChangeStatusById(m.ID, nt.JobStatusRunning); err != nil {
return err
}
}
return nil
}
+84
View File
@@ -0,0 +1,84 @@
package service
import (
"context"
"fmt"
"next-terminal/server/common"
"next-terminal/server/common/nt"
"strings"
"time"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
type CheckAssetStatusJob struct {
ID string
Mode string
ResourceIds string
Metadata string
}
func (r CheckAssetStatusJob) Run() {
if r.ID == "" {
return
}
var assets []model.Asset
if r.Mode == nt.JobModeAll {
assets, _ = repository.AssetRepository.FindAll(context.TODO())
} else {
assets, _ = repository.AssetRepository.FindByIds(context.TODO(), strings.Split(r.ResourceIds, ","))
}
if len(assets) == 0 {
return
}
msgChan := make(chan string)
for i := range assets {
asset := assets[i]
go func() {
t1 := time.Now()
var (
msg string
ip = asset.IP
port = asset.Port
)
active, err := AssetService.CheckStatus(&asset, ip, port)
elapsed := time.Since(t1)
if err == nil {
msg = fmt.Sprintf("资产「%v」存活状态检测完成,存活「%v」,耗时「%v」", asset.Name, active, elapsed)
} else {
msg = fmt.Sprintf("资产「%v」存活状态检测完成,存活「%v」,耗时「%v」,原因: %v", asset.Name, active, elapsed, err.Error())
}
var message = ""
if !active && err != nil {
message = err.Error()
}
_ = repository.AssetRepository.UpdateActiveById(context.TODO(), active, message, asset.ID)
log.Debug(msg)
msgChan <- msg
}()
}
var message = ""
for i := 0; i < len(assets); i++ {
message += <-msgChan + "\n"
}
_ = repository.JobRepository.UpdateLastUpdatedById(context.TODO(), r.ID)
jobLog := model.JobLog{
ID: utils.UUID(),
JobId: r.ID,
Timestamp: common.NowJsonTime(),
Message: message,
}
_ = repository.JobLogRepository.Create(context.TODO(), &jobLog)
}
+199
View File
@@ -0,0 +1,199 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"next-terminal/server/common"
"next-terminal/server/common/nt"
"next-terminal/server/common/term"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
type ShellJob struct {
ID string
Mode string
ResourceIds string
Metadata string
}
type MetadataShell struct {
Shell string
}
func (r ShellJob) Run() {
if r.ID == "" {
return
}
switch r.Mode {
case nt.JobModeAll:
assets, _ := repository.AssetRepository.FindByProtocol(context.TODO(), "ssh")
r.executeShellByAssets(assets)
case nt.JobModeCustom:
assets, _ := repository.AssetRepository.FindByProtocolAndIds(context.TODO(), "ssh", strings.Split(r.ResourceIds, ","))
r.executeShellByAssets(assets)
case nt.JobModeSelf:
r.executeShellByLocal()
}
}
func (r ShellJob) executeShellByAssets(assets []model.Asset) {
if len(assets) == 0 {
return
}
var metadataShell MetadataShell
err := json.Unmarshal([]byte(r.Metadata), &metadataShell)
if err != nil {
log.Error("JSON数据解析失败", log.String("err", err.Error()))
return
}
msgChan := make(chan string)
for i := range assets {
asset, err := AssetService.FindByIdAndDecrypt(context.TODO(), assets[i].ID)
if err != nil {
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询数据异常「%v」", assets[i].Name, err.Error())
return
}
var (
username = asset.Username
password = asset.Password
privateKey = asset.PrivateKey
passphrase = asset.Passphrase
ip = asset.IP
port = asset.Port
)
if asset.AccountType == "credential" {
credential, err := CredentialService.FindByIdAndDecrypt(context.TODO(), asset.CredentialId)
if err != nil {
msgChan <- fmt.Sprintf("资产「%v」Shell执行失败,查询授权凭证数据异常「%v」", assets[i].Name, err.Error())
return
}
if credential.Type == nt.Custom {
username = credential.Username
password = credential.Password
} else {
username = credential.Username
privateKey = credential.PrivateKey
passphrase = credential.Passphrase
}
}
go func() {
t1 := time.Now()
result, err := execute(metadataShell.Shell, asset.AccessGatewayId, ip, port, username, password, privateKey, passphrase)
elapsed := time.Since(t1)
var msg string
if err != nil {
if errors.Is(gorm.ErrRecordNotFound, err) {
msg = fmt.Sprintf("资产「%v」Shell执行失败,请检查资产所关联接入网关是否存在,耗时「%v」", asset.Name, elapsed)
} else {
msg = fmt.Sprintf("资产「%v」Shell执行失败,错误内容为:「%v」,耗时「%v」", asset.Name, err.Error(), elapsed)
}
log.Debug(msg)
} else {
msg = fmt.Sprintf("资产「%v」Shell执行成功,返回值「%v」,耗时「%v」", asset.Name, result, elapsed)
log.Debug(msg)
}
msgChan <- msg
}()
}
var message = ""
for i := 0; i < len(assets); i++ {
message += <-msgChan + "\n"
}
_ = repository.JobRepository.UpdateLastUpdatedById(context.TODO(), r.ID)
jobLog := model.JobLog{
ID: utils.UUID(),
JobId: r.ID,
Timestamp: common.NowJsonTime(),
Message: message,
}
_ = repository.JobLogRepository.Create(context.TODO(), &jobLog)
}
func (r ShellJob) executeShellByLocal() {
var metadataShell MetadataShell
err := json.Unmarshal([]byte(r.Metadata), &metadataShell)
if err != nil {
log.Error("JSON数据解析失败", log.String("err", err.Error()))
return
}
now := time.Now()
var msg = ""
log.Debug("run local command", log.String("cmd", metadataShell.Shell))
output, outerr, err := utils.Exec(metadataShell.Shell)
if err != nil {
msg = fmt.Sprintf("命令执行失败,错误内容为:「%v」,耗时「%v」", err.Error(), time.Since(now).String())
} else {
msg = fmt.Sprintf("命令执行成功,stdout 返回值「%v」,stderr 返回值「%v」,耗时「%v」", output, outerr, time.Since(now).String())
}
_ = repository.JobRepository.UpdateLastUpdatedById(context.Background(), r.ID)
jobLog := model.JobLog{
ID: utils.UUID(),
JobId: r.ID,
Timestamp: common.NowJsonTime(),
Message: msg,
}
_ = repository.JobLogRepository.Create(context.Background(), &jobLog)
}
func execute(shell, accessGatewayId, ip string, port int, username, password, privateKey, passphrase string) (string, error) {
if accessGatewayId != "" && accessGatewayId != "-" {
g, err := GatewayService.GetGatewayById(accessGatewayId)
if err != nil {
return "", err
}
uuid := utils.UUID()
defer g.CloseSshTunnel(uuid)
exposedIP, exposedPort, err := g.OpenSshTunnel(uuid, ip, port)
if err != nil {
return "", err
}
return ExecCommandBySSH(shell, exposedIP, exposedPort, username, password, privateKey, passphrase)
} else {
return ExecCommandBySSH(shell, ip, port, username, password, privateKey, passphrase)
}
}
func ExecCommandBySSH(cmd, ip string, port int, username, password, privateKey, passphrase string) (result string, err error) {
sshClient, err := term.NewSshClient(ip, port, username, password, privateKey, passphrase)
if err != nil {
return "", err
}
session, err := sshClient.NewSession()
if err != nil {
return "", err
}
defer func() {
_ = session.Close()
}()
//执行远程命令
combo, err := session.CombinedOutput(cmd)
if err != nil {
return "", err
}
return string(combo), nil
}
+249
View File
@@ -0,0 +1,249 @@
package service
import (
"context"
"errors"
"net"
"strings"
"time"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var LoginPolicyService = new(loginPolicyService)
type loginPolicyService struct {
baseService
}
func (s loginPolicyService) Create(c context.Context, m *model.LoginPolicy) error {
return s.Transaction(c, func(ctx context.Context) error {
if err := repository.LoginPolicyRepository.Create(ctx, m); err != nil {
return err
}
if len(m.TimePeriod) > 0 {
for i := range m.TimePeriod {
m.TimePeriod[i].ID = utils.UUID()
m.TimePeriod[i].LoginPolicyId = m.ID
}
if err := repository.TimePeriodRepository.CreateInBatches(ctx, m.TimePeriod); err != nil {
return err
}
}
return nil
})
}
func (s loginPolicyService) DeleteByIds(ctx context.Context, ids []string) error {
return s.Transaction(ctx, func(ctx context.Context) error {
for _, id := range ids {
if err := repository.LoginPolicyRepository.DeleteById(ctx, id); err != nil {
return err
}
if err := repository.LoginPolicyUserRefRepository.DeleteByLoginPolicyId(ctx, id); err != nil {
return err
}
if err := repository.TimePeriodRepository.DeleteByLoginPolicyId(ctx, id); err != nil {
return err
}
}
return nil
})
}
func (s loginPolicyService) UpdateById(ctx context.Context, m *model.LoginPolicy, id string) error {
return s.Transaction(ctx, func(ctx context.Context) error {
if err := repository.LoginPolicyRepository.UpdateById(ctx, m, id); err != nil {
return err
}
if err := repository.TimePeriodRepository.DeleteByLoginPolicyId(ctx, id); err != nil {
return err
}
if len(m.TimePeriod) > 0 {
for i := range m.TimePeriod {
m.TimePeriod[i].ID = utils.UUID()
m.TimePeriod[i].LoginPolicyId = m.ID
}
if err := repository.TimePeriodRepository.CreateInBatches(ctx, m.TimePeriod); err != nil {
return err
}
}
return nil
})
}
func (s loginPolicyService) FindById(ctx context.Context, id string) (*model.LoginPolicy, error) {
policy, err := repository.LoginPolicyRepository.FindById(ctx, id)
if err != nil {
return nil, err
}
timePeriods, err := repository.TimePeriodRepository.FindByLoginPolicyId(ctx, id)
if err != nil {
return nil, err
}
policy.TimePeriod = timePeriods
return &policy, nil
}
func (s loginPolicyService) Check(userId, clientIp string) error {
ctx := context.Background()
// 按照优先级倒排进行查询
policies, err := repository.LoginPolicyRepository.FindByUserId(ctx, userId)
if err != nil {
return err
}
if len(policies) == 0 {
return nil
}
if err := s.checkClientIp(policies, clientIp); err != nil {
return err
}
if err := s.checkWeekDay(policies); err != nil {
return err
}
return nil
}
func (s loginPolicyService) checkClientIp(policies []model.LoginPolicy, clientIp string) error {
var pass = true
// 优先级低的先进行判断
for _, policy := range policies {
if !policy.Enabled {
continue
}
ipGroups := strings.Split(policy.IpGroup, ",")
for _, group := range ipGroups {
if strings.Contains(group, "/") {
// CIDR
_, ipNet, err := net.ParseCIDR(group)
if err != nil {
continue
}
if !ipNet.Contains(net.ParseIP(clientIp)) {
continue
}
} else if strings.Contains(group, "-") {
// 范围段
split := strings.Split(group, "-")
if len(split) < 2 {
continue
}
start := split[0]
end := split[1]
intReqIP := utils.IpToInt(clientIp)
if intReqIP < utils.IpToInt(start) || intReqIP > utils.IpToInt(end) {
continue
}
} else {
// IP
if group != clientIp {
continue
}
}
pass = policy.Rule == "allow"
}
}
if !pass {
return errors.New("非常抱歉,您当前使用的IP地址不允许进行登录。")
}
return nil
}
func (s loginPolicyService) checkWeekDay(policies []model.LoginPolicy) error {
// 获取当前日期是星期几
now := time.Now()
weekday := int(now.Weekday())
hwc := now.Format("15:04")
var timePass = true
// 优先级低的先进行判断
for _, policy := range policies {
if !policy.Enabled {
continue
}
timePeriods, err := repository.TimePeriodRepository.FindByLoginPolicyId(context.Background(), policy.ID)
if err != nil {
return err
}
for _, period := range timePeriods {
if weekday != period.Key {
continue
}
if period.Value == "" {
continue
}
// 只处理对应天的数据
times := strings.Split(period.Value, "、")
for _, t := range times {
timeRange := strings.Split(t, "~")
start := timeRange[0]
end := timeRange[1]
if (start == "00:00" && end == "00:00") || (start <= hwc && hwc <= end) {
timePass = policy.Rule == "allow"
}
}
}
}
if !timePass {
return errors.New("非常抱歉,当前时段不允许您进行登录。")
}
return nil
}
func (s loginPolicyService) Bind(ctx context.Context, loginPolicyId string, items []model.LoginPolicyUserRef) error {
return s.Transaction(ctx, func(ctx context.Context) error {
var results []model.LoginPolicyUserRef
for i := range items {
if items[i].UserId == "" {
continue
}
exist, err := repository.UserRepository.ExistById(ctx, items[i].UserId)
if err != nil {
continue
}
if !exist {
continue
}
refId := utils.Sign([]string{items[i].UserId, loginPolicyId})
if err := repository.LoginPolicyUserRefRepository.DeleteId(ctx, refId); err != nil {
return err
}
results = append(results, model.LoginPolicyUserRef{
ID: refId,
UserId: items[i].UserId,
LoginPolicyId: loginPolicyId,
})
}
if len(results) == 0 {
return nil
}
return repository.LoginPolicyUserRefRepository.CreateInBatches(ctx, results)
})
}
func (s loginPolicyService) Unbind(ctx context.Context, loginPolicyId string, items []model.LoginPolicyUserRef) error {
return s.Transaction(ctx, func(ctx context.Context) error {
for i := range items {
if items[i].UserId == "" {
continue
}
if err := repository.LoginPolicyUserRefRepository.DeleteByLoginPolicyIdAndUserId(ctx, loginPolicyId, items[i].UserId); err != nil {
return err
}
}
return nil
})
}
+61
View File
@@ -0,0 +1,61 @@
package service
import (
"context"
"fmt"
"net/smtp"
"next-terminal/server/common/nt"
"next-terminal/server/branding"
"next-terminal/server/log"
"next-terminal/server/repository"
"github.com/jordan-wright/email"
)
var MailService = new(mailService)
type mailService struct {
}
func (r mailService) SendMail(to, subject, text string) {
propertiesMap := repository.PropertyRepository.FindAllMap(context.TODO())
host := propertiesMap[nt.MailHost]
port := propertiesMap[nt.MailPort]
username := propertiesMap[nt.MailUsername]
password := propertiesMap[nt.MailPassword]
if host == "" || port == "" || username == "" || password == "" {
log.Warn("邮箱信息不完整,跳过发送邮件。")
return
}
e := email.NewEmail()
e.From = fmt.Sprintf("%s <%s>", branding.Name, username)
e.To = []string{to}
e.Subject = subject
e.Text = []byte(text)
err := e.Send(host+":"+port, smtp.PlainAuth("", username, password, host))
if err != nil {
log.Error("邮件发送失败", log.String("err", err.Error()))
}
}
func (r mailService) SendTestMail(req map[string]interface{}) error {
host, _ := req["mail-host"].(string)
port, _ := req["mail-port"].(string)
username, _ := req["mail-username"].(string)
password, _ := req["mail-password"].(string)
to, _ := req["mail-to"].(string)
if host == "" || port == "" || username == "" || to == "" {
return fmt.Errorf("邮箱信息不完整")
}
e := email.NewEmail()
e.From = fmt.Sprintf("%s <%s>", branding.Name, username)
e.To = []string{to}
e.Subject = "Next Terminal Test Mail"
e.Text = []byte("This is a test email from Next Terminal.")
return e.Send(host+":"+port, smtp.PlainAuth("", username, password, host))
}
+81
View File
@@ -0,0 +1,81 @@
package service
import (
"github.com/ucarion/urlpath"
"next-terminal/server/dto"
"next-terminal/server/model"
)
var MenuService = &menuService{}
type menuService struct {
menuPermissions map[string][]*urlpath.Path
treeMenus []*dto.TreeMenu
}
func (s *menuService) Init() error {
if s.menuPermissions == nil {
s.menuPermissions = make(map[string][]*urlpath.Path)
}
// 重载权限路径
for _, menu := range DefaultMenu {
var permissions []*urlpath.Path
for _, permission := range menu.Permissions {
path := urlpath.New(permission.Path)
permissions = append(permissions, &path)
}
s.menuPermissions[menu.ID] = permissions
}
// 重载菜单树缓存
for _, menu := range DefaultMenu {
if menu.ParentId == "root" {
childMenus := getChildren(DefaultMenu, menu.ID)
p := &dto.TreeMenu{
Title: menu.Name,
Key: menu.ID,
IsLeaf: len(childMenus) == 0,
Children: childMenus,
}
s.treeMenus = append(s.treeMenus, p)
}
}
return nil
}
func getChildren(menus []*model.Menu, parentId string) []dto.TreeMenu {
var children []dto.TreeMenu
for _, menu := range menus {
if menu.ParentId == parentId {
childMenus := getChildren(DefaultMenu, menu.ID)
p := dto.TreeMenu{
Title: menu.Name,
Key: menu.ID,
IsLeaf: len(childMenus) == 0,
Children: childMenus,
}
children = append(children, p)
}
}
return children
}
func (s *menuService) GetPermissionByMenu(menu string) []*urlpath.Path {
item, ok := s.menuPermissions[menu]
if ok {
return item
}
return nil
}
func (s *menuService) GetTreeMenus() []*dto.TreeMenu {
return s.treeMenus
}
func (s *menuService) GetMenus() (items []string) {
for _, menu := range DefaultMenu {
items = append(items, menu.ID)
}
return items
}
+459
View File
@@ -0,0 +1,459 @@
package service
import "next-terminal/server/model"
var DefaultMenu = []*model.Menu{
model.NewMenu("dashboard", "控制面板", "root",
model.NewPermission("GET", "/overview/counter"),
model.NewPermission("GET", "/overview/asset"),
model.NewPermission("GET", "/overview/date-counter"),
),
model.NewMenu("resource", "资源管理", "root"),
model.NewMenu("asset", "资产管理", "resource",
model.NewPermission("GET", "/assets/paging"),
model.NewPermission("GET", "/tags"),
),
model.NewMenu("asset-access", "接入", "asset",
model.NewPermission("POST", "/sessions"),
model.NewPermission("GET", "/sessions/:id/tunnel"),
model.NewPermission("GET", "/sessions/:id/ssh"),
model.NewPermission("GET", "/sessions/:id/stats"),
model.NewPermission("POST", "/sessions/:id/connect"),
model.NewPermission("POST", "/sessions/:id/resize"),
model.NewPermission("POST", "/sessions/:id/ls"),
model.NewPermission("GET", "/sessions/:id/download"),
model.NewPermission("POST", "/sessions/:id/upload"),
model.NewPermission("POST", "/sessions/:id/edit"),
model.NewPermission("POST", "/sessions/:id/mkdir"),
model.NewPermission("POST", "/sessions/:id/rm"),
model.NewPermission("POST", "/sessions/:id/rename"),
),
model.NewMenu("asset-add", "新建", "asset",
model.NewPermission("POST", "/assets"),
model.NewPermission("GET", "/access-gateways"),
model.NewPermission("GET", "/credentials"),
),
model.NewMenu("asset-edit", "编辑", "asset",
model.NewPermission("GET", "/assets/:id"),
model.NewPermission("PUT", "/assets/:id"),
model.NewPermission("GET", "/access-gateways"),
model.NewPermission("GET", "/credentials"),
),
model.NewMenu("asset-del", "删除", "asset",
model.NewPermission("DELETE", "/assets/:id"),
),
model.NewMenu("asset-copy", "复制", "asset",
model.NewPermission("GET", "/assets/:id"),
model.NewPermission("POST", "/assets"),
),
model.NewMenu("asset-conn-test", "连通性测试", "asset",
model.NewPermission("POST", "/assets/:id/tcping"),
),
model.NewMenu("asset-import", "导入资产", "asset",
model.NewPermission("POST", "/assets/import"),
),
model.NewMenu("asset-change-owner", "更换所有者", "asset",
model.NewPermission("GET", "/users"),
model.NewPermission("POST", "/assets/:id/change-owner"),
),
model.NewMenu("asset-detail", "详情", "asset",
model.NewPermission("GET", "/assets/:id"),
),
model.NewMenu("asset-authorised-user", "资产授权用户", "asset-detail",
model.NewPermission("GET", "/authorised/users/paging"),
model.NewPermission("GET", "/authorised/selected"),
model.NewPermission("GET", "/users"),
model.NewPermission("GET", "/strategies"),
model.NewPermission("GET", "/command-filters"),
model.NewPermission("POST", "/authorised/users"),
),
model.NewMenu("asset-authorised-user-add", "增加授权", "asset-authorised-user",
model.NewPermission("POST", "/authorised/:id/users"),
),
model.NewMenu("asset-authorised-user-del", "移除授权", "asset-authorised-user",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("asset-authorised-user-group", "资产授权用户组", "asset-detail",
model.NewPermission("GET", "/authorised/user-groups/paging"),
model.NewPermission("GET", "/authorised/selected"),
model.NewPermission("GET", "/user-groups"),
model.NewPermission("GET", "/strategies"),
model.NewPermission("GET", "/command-filters"),
model.NewPermission("POST", "/authorised/user-groups"),
),
model.NewMenu("asset-authorised-user-group-add", "增加授权", "asset-authorised-user-group",
model.NewPermission("POST", "/authorised/:id/user-groups"),
),
model.NewMenu("asset-authorised-user-group-del", "移除授权", "asset-authorised-user-group",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("credential", "授权凭证", "resource",
model.NewPermission("GET", "/credentials/paging"),
),
model.NewMenu("credential-add", "增加", "credential",
model.NewPermission("POST", "/credentials"),
),
model.NewMenu("credential-del", "删除", "credential",
model.NewPermission("DELETE", "/credentials/:id"),
),
model.NewMenu("credential-edit", "修改", "credential",
model.NewPermission("POST", "/credentials/:id"),
),
model.NewMenu("command", "动态指令", "resource",
model.NewPermission("GET", "/commands/paging"),
),
model.NewMenu("command-add", "增加", "command",
model.NewPermission("POST", "/commands"),
),
model.NewMenu("command-edit", "修改", "command",
model.NewPermission("PUT", "/commands/:id"),
),
model.NewMenu("command-del", "删除", "command",
model.NewPermission("DELETE", "/commands/:id"),
),
model.NewMenu("command-exec", "执行", "command",
model.NewPermission("GET", "/assets/paging"),
model.NewPermission("GET", "/tags"),
model.NewPermission("POST", "/sessions"),
model.NewPermission("GET", "/term"),
),
model.NewMenu("command-change-owner", "更换所有者", "command",
model.NewPermission("GET", "/users"),
model.NewPermission("POST", "/commands/:id/change-owner"),
),
model.NewMenu("access-gateway", "接入网关", "resource",
model.NewPermission("GET", "/access-gateways/paging"),
),
model.NewMenu("access-gateway-add", "增加", "access-gateway",
model.NewPermission("POST", "/access-gateways"),
),
model.NewMenu("access-gateway-del", "删除", "access-gateway",
model.NewPermission("DELETE", "/access-gateways/:id"),
),
model.NewMenu("access-gateway-edit", "修改", "access-gateway",
model.NewPermission("PUT", "/access-gateways/:id"),
),
model.NewMenu("session-audit", "会话审计", "root"),
model.NewMenu("online-session", "在线会话", "session-audit",
model.NewPermission("GET", "/sessions/paging"),
),
model.NewMenu("online-session-disconnect", "断开", "online-session",
model.NewPermission("GET", "/sessions/:id/disconnect"),
),
model.NewMenu("online-session-monitor", "监控", "online-session",
model.NewPermission("GET", "/sessions/:id/tunnel-monitor"),
model.NewPermission("GET", "/sessions/:id/ssh-monitor"),
),
model.NewMenu("offline-session", "历史会话", "session-audit",
model.NewPermission("GET", "/sessions/paging"),
),
model.NewMenu("offline-session-playback", "回放", "offline-session",
model.NewPermission("GET", "/sessions/:id/recording"),
),
model.NewMenu("offline-session-del", "删除", "offline-session",
model.NewPermission("DELETE", "/sessions/:id"),
),
model.NewMenu("offline-session-clear", "清空", "offline-session",
model.NewPermission("POST", "/sessions/clear"),
),
model.NewMenu("offline-session-command", "命令记录", "offline-session",
model.NewPermission("GET", "/sessions/:id/commands/paging"),
),
model.NewMenu("offline-session-reviewed", "标记已读", "offline-session"), // TODO
model.NewMenu("offline-session-unreviewed", "标记未读", "offline-session"), // TODO
model.NewMenu("offline-session-reviewed-all", "全部标记已读", "offline-session"), // TODO
model.NewMenu("log-audit", "日志审计", "root"),
model.NewMenu("login-log", "登录日志", "log-audit",
model.NewPermission("GET", "/login-logs/paging"),
),
model.NewMenu("login-log-del", "删除", "login-log",
model.NewPermission("DELETE", "/login-logs/:id"),
),
model.NewMenu("login-log-clear", "清空", "login-log",
model.NewPermission("POST", "/login-logs/clear"),
),
model.NewMenu("storage-log", "文件日志", "log-audit",
model.NewPermission("GET", "/storage-logs/paging"),
),
model.NewMenu("storage-log-del", "删除", "storage-log",
model.NewPermission("DELETE", "/storage-logs/:id"),
),
model.NewMenu("storage-log-clear", "清空", "storage-log",
model.NewPermission("POST", "/storage-logs/clear"),
),
model.NewMenu("session-command", "命令日志", "log-audit",
model.NewPermission("GET", "/session-commands/paging"),
),
model.NewMenu("ops", "系统运维", "root"),
model.NewMenu("job", "计划任务", "ops",
model.NewPermission("GET", "/jobs/paging"),
),
model.NewMenu("job-add", "增加", "job",
model.NewPermission("POST", "/jobs"),
model.NewPermission("GET", "/assets/paging"),
),
model.NewMenu("job-del", "删除", "job",
model.NewPermission("DELETE", "/jobs/:id"),
),
model.NewMenu("job-edit", "修改", "job",
model.NewPermission("PUT", "/jobs/:id"),
model.NewPermission("GET", "/assets/paging"),
),
model.NewMenu("job-run", "执行", "job",
model.NewPermission("POST", "/jobs/:id/exec"),
),
model.NewMenu("job-change-status", "开启/关闭", "job",
model.NewPermission("POST", "/jobs/:id/change-status"),
),
model.NewMenu("job-log", "日志", "job",
model.NewPermission("GET", "/jobs/:id/logs/paging"),
),
model.NewMenu("job-log-clear", "日志清空", "job",
model.NewPermission("DELETE", "/jobs/:id/logs"),
),
model.NewMenu("storage", "磁盘空间", "ops",
model.NewPermission("GET", "/storages/paging"),
),
model.NewMenu("storage-add", "增加", "storage",
model.NewPermission("POST", "/storages"),
),
model.NewMenu("storage-del", "删除", "storage",
model.NewPermission("DELETE", "/storages/:id"),
),
model.NewMenu("storage-edit", "修改", "storage",
model.NewPermission("PUT", "/storages/:id"),
),
model.NewMenu("storage-browse", "浏览", "storage",
model.NewPermission("GET", "/storages/:id/ls"),
),
model.NewMenu("storage-browse-download", "下载", "storage-browse",
model.NewPermission("GET", "/storages/:id/download"),
),
model.NewMenu("storage-browse-upload", "上传", "storage-browse",
model.NewPermission("POST", "/storages/:id/upload"),
),
model.NewMenu("storage-browse-mkdir", "创建文件夹", "storage-browse",
model.NewPermission("POST", "/storages/:id/mkdir"),
),
model.NewMenu("storage-browse-rm", "删除", "storage-browse",
model.NewPermission("POST", "/storages/:id/rm"),
),
model.NewMenu("storage-browse-rename", "重命名", "storage-browse",
model.NewPermission("POST", "/storages/:id/rename"),
),
model.NewMenu("storage-browse-edit", "编辑", "storage-browse",
model.NewPermission("POST", "/storages/:id/edit"),
),
model.NewMenu("monitoring", "系统监控", "ops",
model.NewPermission("GET", "/overview/ps"),
),
model.NewMenu("security", "安全策略", "root"),
model.NewMenu("access-security", "访问安全", "security",
model.NewPermission("GET", "/securities/paging"),
),
model.NewMenu("access-security-add", "增加", "access-security",
model.NewPermission("POST", "/securities"),
),
model.NewMenu("access-security-del", "删除", "access-security",
model.NewPermission("DELETE", "/securities/:id"),
),
model.NewMenu("access-security-edit", "修改", "access-security",
model.NewPermission("PUT", "/securities/:id"),
),
model.NewMenu("login-policy", "登录策略", "security",
model.NewPermission("GET", "/login-policies/paging"),
),
model.NewMenu("login-policy-add", "增加", "login-policy",
model.NewPermission("POST", "/login-policies"),
),
model.NewMenu("login-policy-del", "删除", "login-policy",
model.NewPermission("DELETE", "/login-policies/:id"),
),
model.NewMenu("login-policy-edit", "修改", "login-policy",
model.NewPermission("PUT", "/login-policies/:id"),
),
model.NewMenu("login-policy-detail", "详情", "login-policy",
model.NewPermission("GET", "/login-policies/:id"),
),
model.NewMenu("login-policy-bind-user", "绑定用户", "login-policy-detail",
model.NewPermission("GET", "/login-policies/:id/users/paging"),
),
model.NewMenu("login-policy-unbind-user", "解绑", "user-login-policy",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("identity", "用户管理", "root"),
model.NewMenu("user", "用户管理", "identity",
model.NewPermission("GET", "/users/paging"),
model.NewPermission("GET", "/roles"),
),
model.NewMenu("user-add", "增加", "user",
model.NewPermission("POST", "/users"),
),
model.NewMenu("user-del", "删除", "user",
model.NewPermission("DELETE", "/users/:id"),
),
model.NewMenu("user-edit", "修改", "user",
model.NewPermission("GET", "/users/:id"),
model.NewPermission("PUT", "/users/:id"),
),
model.NewMenu("user-change-password", "修改密码", "user",
model.NewPermission("POST", "/users/:id/change-password"),
),
model.NewMenu("user-enable-disable", "启用/禁用", "user",
model.NewPermission("PATCH", "/users/:id/status"),
),
model.NewMenu("user-reset-totp", "重置双因素认证", "user",
model.NewPermission("POST", "/users/:id/reset-totp"),
),
model.NewMenu("user-detail", "用户详情", "user",
model.NewPermission("GET", "/users/:id"),
model.NewPermission("GET", "/authorised/assets/paging"),
),
model.NewMenu("user-authorised-asset", "授权的资产", "user-detail",
model.NewPermission("GET", "/authorised/assets/paging"),
),
model.NewMenu("user-bind-asset", "授权", "user-authorised-asset",
model.NewPermission("GET", "/authorised/selected"),
model.NewPermission("GET", "/assets"),
model.NewPermission("GET", "/strategies"),
model.NewPermission("GET", "/command-filters"),
),
model.NewMenu("user-unbind-asset", "移除", "user-authorised-asset",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("user-login-policy", "登录策略", "user-detail",
model.NewPermission("GET", "/login-policies/paging", "userId"),
),
model.NewMenu("user-unbind-login-policy", "解绑", "user-login-policy",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("role", "角色管理", "identity",
model.NewPermission("GET", "/roles/paging"),
),
model.NewMenu("role-add", "增加", "role",
model.NewPermission("POST", "/roles"),
),
model.NewMenu("role-del", "删除", "role",
model.NewPermission("DELETE", "/roles/:id"),
),
model.NewMenu("role-edit", "修改", "role",
model.NewPermission("GET", "/roles/:id"),
model.NewPermission("PUT", "/roles/:id"),
),
model.NewMenu("role-detail", "详情", "role",
model.NewPermission("GET", "/roles/:id"),
model.NewPermission("GET", "/menus"),
),
model.NewMenu("user-group", "用户组管理", "identity",
model.NewPermission("GET", "/user-groups/paging"),
),
model.NewMenu("user-group-add", "增加", "user-group",
model.NewPermission("POST", "/user-groups"),
),
model.NewMenu("user-group-del", "删除", "user-group",
model.NewPermission("DELETE", "/user-groups:/id"),
),
model.NewMenu("user-group-edit", "修改", "user-group",
model.NewPermission("GET", "/user-groups/:id"),
model.NewPermission("PUT", "/user-groups/:id"),
),
model.NewMenu("user-group-detail", "详情", "user-group",
model.NewPermission("GET", "/user-groups/:id"),
),
model.NewMenu("user-group-authorised-asset", "授权的资产", "user-group",
model.NewPermission("GET", "/authorised/assets/paging"),
),
model.NewMenu("user-group-bind-asset", "授权", "user-group-authorised-asset",
model.NewPermission("GET", "/authorised/selected"),
model.NewPermission("GET", "/assets"),
model.NewPermission("GET", "/strategies"),
model.NewPermission("GET", "/command-filters"),
),
model.NewMenu("user-group-unbind-asset", "移除", "user-group-authorised-asset",
model.NewPermission("DELETE", "/authorised/:id"),
),
model.NewMenu("authorised", "授权策略", "root"),
model.NewMenu("command-filter", "命令过滤", "authorised",
model.NewPermission("GET", "/command-filters/paging"),
),
model.NewMenu("command-filter-add", "增加", "command-filter",
model.NewPermission("POST", "/command-filters"),
),
model.NewMenu("command-filter-del", "删除", "command-filter",
model.NewPermission("DELETE", "/command-filters/:id"),
),
model.NewMenu("command-filter-edit", "编辑", "command-filter",
model.NewPermission("GET", "/command-filters/:id"),
model.NewPermission("PUT", "/command-filters/:id"),
),
model.NewMenu("command-filter-detail", "详情", "command-filter",
model.NewPermission("GET", "/command-filters/:id"),
),
model.NewMenu("command-filter-rule", "规则", "command-filter-detail",
model.NewPermission("GET", "/command-filter-rules/:id"),
),
model.NewMenu("command-filter-rule-add", "增加", "command-filter-rule",
model.NewPermission("POST", "/command-filter-rules"),
),
model.NewMenu("command-filter-rule-put", "修改", "command-filter-rule",
model.NewPermission("GET", "/command-filter-rules/:id"),
model.NewPermission("PUT", "/command-filter-rules/:id"),
),
model.NewMenu("command-filter-rule-del", "删除", "command-filter-rule",
model.NewPermission("DELETE", "/command-filter-rules/:id"),
),
model.NewMenu("strategy", "授权策略", "authorised",
model.NewPermission("GET", "/strategies/paging"),
),
model.NewMenu("strategy-add", "增加", "strategy",
model.NewPermission("POST", "/strategies"),
),
model.NewMenu("strategy-edit", "修改", "strategy",
model.NewPermission("GET", "/strategies/:id"),
model.NewPermission("PUT", "/strategies/:id"),
),
model.NewMenu("strategy-del", "删除", "strategy",
model.NewPermission("DELETE", "/strategies/:id"),
),
model.NewMenu("strategy-detail", "详情", "strategy",
model.NewPermission("GET", "/strategies/:id"),
),
model.NewMenu("setting", "系统设置", "root",
model.NewPermission("GET", "/properties"),
model.NewPermission("PUT", "/properties"),
model.NewPermission("POST", "/ldap-user-sync"),
model.NewPermission("GET", "/license"),
model.NewPermission("POST", "/license"),
model.NewPermission("GET", "/license/machine-id"),
),
model.NewMenu("info", "个人中心", "root"),
}
+109
View File
@@ -0,0 +1,109 @@
package service
import (
"context"
"errors"
"strings"
"next-terminal/server/branding"
"next-terminal/server/common"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
type resourceSharer struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
ResourceId string `gorm:"index,type:varchar(36)" json:"resourceId"`
ResourceType string `gorm:"index,type:varchar(36)" json:"resourceType"`
StrategyId string `gorm:"index,type:varchar(36)" json:"strategyId"`
UserId string `gorm:"index,type:varchar(36)" json:"userId"`
UserGroupId string `gorm:"index,type:varchar(36)" json:"userGroupId"`
}
var MigrateService = &migrateService{}
type migrateService struct {
baseService
}
func (s *migrateService) Migrate() error {
var needMigrate = false
var localVersion = ""
property, err := repository.PropertyRepository.FindByName(context.Background(), "version")
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 未获取到版本数据
needMigrate = true
} else {
localVersion = property.Value
// 数据库版本小于当前版本
needMigrate = strings.Compare(localVersion, branding.Version) < 0
}
if !needMigrate {
return nil
}
if err := s.migrateFormV127to130(localVersion); err != nil {
return err
}
return PropertyService.Update(map[string]interface{}{"version": branding.Version})
}
func (s *migrateService) migrateFormV127to130(localVersion string) (err error) {
if !strings.Contains(localVersion, "beta") && strings.Compare(localVersion, "v1.3.0") > 0 {
return nil
}
err = env.GetDB().Exec(`update strategies set create_dir = 0 where create_dir = ''`).Error
if err != nil {
return err
}
ctx := context.Background()
var results []resourceSharer
err = env.GetDB().Raw(`select * from resource_sharers where resource_type = 'asset'`).Find(&results).Error
if err != nil {
// 数据库不存在
return nil
}
// 证明存在旧数据库,执行迁移
var items []model.Authorised
for _, result := range results {
assetId := result.ResourceId
strategyId := result.StrategyId
userId := result.UserId
userGroupId := result.UserGroupId
id := utils.Sign([]string{assetId, userId, userGroupId})
if err := repository.AuthorisedRepository.DeleteById(ctx, id); err != nil {
return err
}
authorised := model.Authorised{
ID: id,
AssetId: assetId,
CommandFilterId: "",
StrategyId: strategyId,
UserId: userId,
UserGroupId: userGroupId,
Created: common.NowJsonTime(),
}
items = append(items, authorised)
}
err = repository.AuthorisedRepository.CreateInBatches(ctx, items)
if err != nil {
return err
}
// 删除旧数据库
return env.GetDB().Exec(`drop table resource_sharers`).Error
}
+119
View File
@@ -0,0 +1,119 @@
package service
import (
"context"
"errors"
"fmt"
"next-terminal/server/common/guacamole"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"gorm.io/gorm"
)
var PropertyService = new(propertyService)
type propertyService struct {
baseService
}
var deprecatedPropertyNames = []string{
guacamole.EnableDrive,
guacamole.DrivePath,
guacamole.DriveName,
guacamole.DisableGlyphCaching,
guacamole.CreateRecordingPath,
}
var defaultProperties = map[string]string{
guacamole.EnableRecording: "true",
guacamole.FontName: "menlo",
guacamole.FontSize: "12",
guacamole.ColorScheme: "gray-black",
guacamole.EnableWallpaper: "true",
guacamole.EnableTheming: "true",
guacamole.EnableFontSmoothing: "true",
guacamole.EnableFullWindowDrag: "true",
guacamole.EnableDesktopComposition: "true",
guacamole.EnableMenuAnimations: "true",
guacamole.DisableBitmapCaching: "false",
guacamole.DisableOffscreenCaching: "false",
"cron-log-saved-limit": "360",
"login-log-saved-limit": "360",
"session-saved-limit": "360",
"user-default-storage-size": "5120",
}
func (service propertyService) InitProperties() error {
propertyMap := repository.PropertyRepository.FindAllMap(context.TODO())
for name, value := range defaultProperties {
if err := service.CreateIfAbsent(propertyMap, name, value); err != nil {
return err
}
}
return nil
}
func (service propertyService) CreateIfAbsent(propertyMap map[string]string, name, value string) error {
if len(propertyMap[name]) == 0 {
property := model.Property{
Name: name,
Value: value,
}
return repository.PropertyRepository.Create(context.TODO(), &property)
}
return nil
}
func (service propertyService) DeleteDeprecatedProperty() error {
propertyMap := repository.PropertyRepository.FindAllMap(context.TODO())
for _, name := range deprecatedPropertyNames {
if propertyMap[name] == "" {
continue
}
if err := repository.PropertyRepository.DeleteByName(context.TODO(), name); err != nil {
return err
}
}
return nil
}
func (service propertyService) Update(item map[string]interface{}) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
for key := range item {
value := fmt.Sprintf("%v", item[key])
if value == "" {
value = "-"
}
property := model.Property{
Name: key,
Value: value,
}
if key == "enable-ldap" && value == "false" {
if err := UserService.DeleteALlLdapUser(c); err != nil {
return err
}
}
_, err := repository.PropertyRepository.FindByName(c, key)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
if err := repository.PropertyRepository.Create(c, &property); err != nil {
return err
}
} else {
if err := repository.PropertyRepository.UpdateByName(c, &property, key); err != nil {
return err
}
}
}
return nil
})
}
+268
View File
@@ -0,0 +1,268 @@
package service
import (
"context"
"errors"
"sync"
"next-terminal/server/common/sets"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var RoleService = new(roleService)
type roleService struct {
baseService
roleMenus sync.Map
}
func (s *roleService) Init() error {
ctx := context.Background()
// 创建默认的角色
if err := s.CreateDefaultRoles(); err != nil {
return err
}
// 重载角色对应权限的缓存
roles, err := repository.RoleRepository.FindAll(ctx)
if err != nil {
return err
}
for _, role := range roles {
refs, err := s.FindMenuByRoleId(ctx, role.ID)
if err != nil {
return err
}
var menus []string
for _, ref := range refs {
menus = append(menus, ref.MenuId)
}
s.setRoleMenus(role.ID, menus)
}
return nil
}
func (s *roleService) mapRoleMenus(keys []string) []model.RoleMenuRef {
var roleMenus []model.RoleMenuRef
for _, key := range keys {
roleMenus = append(roleMenus, model.RoleMenuRef{
MenuId: key,
Checked: true,
})
}
return roleMenus
}
func (s *roleService) Create(c context.Context, role *model.Role) error {
return s.Transaction(c, func(ctx context.Context) error {
if err := repository.RoleRepository.Create(ctx, role); err != nil {
return err
}
if err := s.createRolePermissionRefs(ctx, role); err != nil {
return err
}
return nil
})
}
func (s *roleService) createRolePermissionRefs(ctx context.Context, role *model.Role) error {
var menuIds = sets.NewStringSet()
var refIds = sets.NewStringSet()
var refs []*model.RoleMenuRef
for _, menu := range role.Menus {
refId := utils.Sign([]string{role.ID, menu.MenuId})
if refIds.Contains(refId) {
continue
}
ref := &model.RoleMenuRef{
ID: refId,
RoleId: role.ID,
MenuId: menu.MenuId,
Checked: menu.Checked,
}
refs = append(refs, ref)
refIds.Add(ref.ID)
menuIds.Add(menu.MenuId)
}
if err := repository.RoleMenuRefRepository.DeleteByIdIn(ctx, refIds.ToArray()); err != nil {
return err
}
if err := repository.RoleMenuRefRepository.CreateInBatches(ctx, refs); err != nil {
return err
}
s.setRoleMenus(role.ID, menuIds.ToArray())
return nil
}
func (s *roleService) UpdateById(c context.Context, role *model.Role, id string, force bool) error {
return s.Transaction(c, func(ctx context.Context) error {
dbRole, err := repository.RoleRepository.FindById(ctx, id)
if err != nil {
return err
}
if !force {
if !dbRole.Modifiable {
return errors.New("prohibit to modify " + dbRole.Name)
}
}
if err := repository.RoleRepository.UpdateById(ctx, role, id); err != nil {
return err
}
if err := repository.RoleMenuRefRepository.DeleteByRoleId(ctx, id); err != nil {
return err
}
if err := s.createRolePermissionRefs(ctx, role); err != nil {
return err
}
return nil
})
}
func (s *roleService) DeleteByIds(c context.Context, ids []string, force bool) error {
return s.Transaction(c, func(ctx context.Context) error {
for i := range ids {
id := ids[i]
if !force {
role, err := repository.RoleRepository.FindById(ctx, id)
if err != nil {
return err
}
if !role.Deletable {
return errors.New("prohibit to delete " + role.Name)
}
}
if err := repository.RoleRepository.DeleteById(ctx, id); err != nil {
return err
}
if err := repository.RoleMenuRefRepository.DeleteByRoleId(ctx, id); err != nil {
return err
}
if err := repository.UserRoleRefRepository.DeleteByRoleId(ctx, id); err != nil {
return err
}
// 删除缓存
s.removeRole(id)
}
return nil
})
}
func (s *roleService) FindById(ctx context.Context, id string) (*model.Role, error) {
role, err := repository.RoleRepository.FindById(ctx, id)
if err != nil {
return nil, err
}
permissions, err := s.FindMenuByRoleId(ctx, id)
if err != nil {
return nil, err
}
for i := range permissions {
permissions[i].ID = ""
permissions[i].RoleId = ""
}
role.Menus = permissions
return &role, nil
}
func (s *roleService) FindMenuByRoleId(ctx context.Context, id string) ([]model.RoleMenuRef, error) {
refs, err := repository.RoleMenuRefRepository.FindByRoleId(ctx, id)
if err != nil {
return nil, err
}
return refs, nil
}
func (s *roleService) GetRolesByUserId(userId string) ([]string, error) {
refs, err := repository.UserRoleRefRepository.FindByUserId(context.Background(), userId)
if err != nil {
return nil, err
}
var roles []string
for _, ref := range refs {
roles = append(roles, ref.RoleId)
}
return roles, nil
}
func (s *roleService) GetMenuListByRole(role string) []string {
value, ok := s.roleMenus.Load(role)
if ok {
return value.([]string)
}
return nil
}
func (s *roleService) setRoleMenus(role string, items []string) {
s.roleMenus.Store(role, items)
}
func (s *roleService) removeRole(role string) {
s.roleMenus.Delete(role)
}
func (s *roleService) CreateDefaultRoles() error {
var menus []string
for _, menu := range DefaultMenu {
menus = append(menus, menu.ID)
}
var auditPermissions = []string{
"dashboard",
"log-audit",
"online-session",
"offline-session",
"login-log",
"online-session-paging",
"online-session-disconnect",
"online-session-monitor",
"offline-session-paging",
"offline-session-playback",
"offline-session-del",
"offline-session-clear",
"offline-session-reviewed",
"offline-session-unreviewed",
"offline-session-reviewed-all",
"login-log-paging",
"login-log-del",
"login-log-clear",
}
var securityPermissions = []string{
"security",
"access-security-paging",
"access-security-add",
"access-security-edit",
"access-security-del",
}
var DefaultRoles = []*model.Role{
model.NewRole("system-administrator", "系统管理员", "default", false, false, s.mapRoleMenus(menus)),
model.NewRole("audit-administrator", "审计管理员", "default", false, false, s.mapRoleMenus(auditPermissions)),
model.NewRole("security-administrator", "安全管理员", "default", false, false, s.mapRoleMenus(securityPermissions)),
}
ctx := context.Background()
for _, role := range DefaultRoles {
exists, err := repository.RoleRepository.ExistsById(ctx, role.ID)
if err != nil {
return err
}
if exists {
if err := s.UpdateById(ctx, role, role.ID, true); err != nil {
return err
}
continue
}
if err := s.Create(ctx, role); err != nil {
return err
}
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package service
import (
"context"
"next-terminal/server/global/security"
"next-terminal/server/repository"
)
var SecurityService = new(securityService)
type securityService struct{}
func (service securityService) ReloadAccessSecurity() error {
rules, err := repository.SecurityRepository.FindAll(context.TODO())
if err != nil {
return err
}
if len(rules) > 0 {
// 先清空
security.GlobalSecurityManager.Clear()
// 再添加到全局的安全管理器中
for i := 0; i < len(rules); i++ {
rule := &security.Security{
ID: rules[i].ID,
IP: rules[i].IP,
Rule: rules[i].Rule,
Priority: rules[i].Priority,
}
security.GlobalSecurityManager.Add(rule)
}
}
return nil
}
+377
View File
@@ -0,0 +1,377 @@
package service
import (
"context"
"encoding/base64"
"errors"
"next-terminal/server/common/nt"
"os"
"path"
"strconv"
"sync"
"next-terminal/server/common"
"next-terminal/server/common/guacamole"
"next-terminal/server/config"
"next-terminal/server/env"
"next-terminal/server/global/session"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
var SessionService = new(sessionService)
type sessionService struct {
baseService
}
func (service sessionService) FixSessionState() error {
sessions, err := repository.SessionRepository.FindByStatus(context.TODO(), nt.Connected)
if err != nil {
return err
}
if len(sessions) > 0 {
for i := range sessions {
s := model.Session{
Status: nt.Disconnected,
DisconnectedTime: common.NowJsonTime(),
}
_ = repository.SessionRepository.UpdateById(context.TODO(), &s, sessions[i].ID)
}
}
return nil
}
func (service sessionService) EmptyPassword() error {
return repository.SessionRepository.EmptyPassword(context.TODO())
}
func (service sessionService) ClearOfflineSession() error {
sessions, err := repository.SessionRepository.FindByStatus(context.TODO(), nt.Disconnected)
if err != nil {
return err
}
sessionIds := make([]string, 0)
for i := range sessions {
sessionIds = append(sessionIds, sessions[i].ID)
}
return service.DeleteByIds(context.TODO(), sessionIds)
}
func (service sessionService) DeleteByIds(c context.Context, sessionIds []string) error {
recordingPath := config.GlobalCfg.Guacd.Recording
for i := range sessionIds {
if err := os.RemoveAll(path.Join(recordingPath, sessionIds[i])); err != nil {
return err
}
if err := repository.SessionRepository.DeleteById(c, sessionIds[i]); err != nil {
return err
}
}
return nil
}
func (service sessionService) ReviewedAll() error {
sessions, err := repository.SessionRepository.FindAllUnReviewed(context.TODO())
if err != nil {
return err
}
var sessionIds = make([]string, 0)
total := len(sessions)
for i := range sessions {
sessionIds = append(sessionIds, sessions[i].ID)
if i >= 100 && i%100 == 0 {
if err := repository.SessionRepository.UpdateReadByIds(context.TODO(), true, sessionIds); err != nil {
return err
}
sessionIds = nil
} else {
if i == total-1 {
if err := repository.SessionRepository.UpdateReadByIds(context.TODO(), true, sessionIds); err != nil {
return err
}
}
}
}
return nil
}
var mutex sync.Mutex
func (service sessionService) CloseSessionById(sessionId string, code int, reason string) {
mutex.Lock()
defer mutex.Unlock()
nextSession := session.GlobalSessionManager.GetById(sessionId)
if nextSession != nil {
log.Debug("会话关闭", log.String("会话ID", sessionId), log.String("原因", reason))
service.WriteCloseMessage(nextSession, nextSession.Mode, code, reason)
if nextSession.Observer != nil {
nextSession.Observer.Range(func(key string, ob *session.Session) {
service.WriteCloseMessage(ob, ob.Mode, code, reason)
log.Debug("强制踢出会话的观察者", log.String("会话ID", sessionId))
})
}
}
session.GlobalSessionManager.Del(sessionId)
service.DisDBSess(sessionId, code, reason)
}
func (service sessionService) WriteCloseMessage(sess *session.Session, mode string, code int, reason string) {
switch mode {
case nt.Guacd:
err := guacamole.NewInstruction("error", "", strconv.Itoa(code))
_ = sess.WriteString(err.String())
disconnect := guacamole.NewInstruction("disconnect")
_ = sess.WriteString(disconnect.String())
case nt.Native, nt.Terminal:
msg := `0` + reason
_ = sess.WriteString(msg)
}
}
func (service sessionService) DisDBSess(sessionId string, code int, reason string) {
_ = env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
s, err := repository.SessionRepository.FindById(c, sessionId)
if err != nil {
return err
}
if s.Status == nt.Disconnected {
return nil
}
if s.Status == nt.Connecting {
// 会话还未建立成功,无需保留数据
if err := repository.SessionRepository.DeleteById(c, sessionId); err != nil {
return err
}
return nil
}
ss := model.Session{}
ss.ID = sessionId
ss.Status = nt.Disconnected
ss.DisconnectedTime = common.NowJsonTime()
ss.Code = code
ss.Message = reason
ss.Password = "-"
ss.PrivateKey = "-"
ss.Passphrase = "-"
if err := repository.SessionRepository.UpdateById(c, &ss, sessionId); err != nil {
return err
}
return nil
})
}
func (service sessionService) FindByIdAndDecrypt(c context.Context, id string) (o model.Session, err error) {
sess, err := repository.SessionRepository.FindById(c, id)
if err != nil {
return o, err
}
if err := service.Decrypt(&sess); err != nil {
return o, err
}
return sess, nil
}
func (service sessionService) Decrypt(item *model.Session) error {
if item.Password != "" && item.Password != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Password)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil {
return err
}
item.Password = string(decryptedCBC)
}
if item.PrivateKey != "" && item.PrivateKey != "-" {
origData, err := base64.StdEncoding.DecodeString(item.PrivateKey)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil {
return err
}
item.PrivateKey = string(decryptedCBC)
}
if item.Passphrase != "" && item.Passphrase != "-" {
origData, err := base64.StdEncoding.DecodeString(item.Passphrase)
if err != nil {
return err
}
decryptedCBC, err := utils.AesDecryptCBC(origData, config.GlobalCfg.EncryptionPassword)
if err != nil {
return err
}
item.Passphrase = string(decryptedCBC)
}
return nil
}
func (service sessionService) renderBoolToStr(b *bool) string {
if *(b) == true {
return "1"
}
return "0"
}
func (service sessionService) Create(clientIp, assetId, mode string, user *model.User) (*model.Session, error) {
asset, err := repository.AssetRepository.FindById(context.TODO(), assetId)
if err != nil {
return nil, err
}
var (
upload = "1"
download = "1"
_delete = "1"
rename = "1"
edit = "1"
fileSystem = "1"
_copy = "1"
paste = "1"
)
if asset.Owner != user.ID && nt.TypeUser == user.Type {
// 普通用户访问非自己创建的资产需要校验权限
authorised, err := AuthorisedService.GetAuthorised(user.ID, assetId)
if err != nil {
return nil, err
}
if authorised == nil || authorised.ID == "" {
return nil, errors.New("您没有权限访问此资产")
}
strategyId := authorised.StrategyId
if strategyId != "" {
strategy, err := repository.StrategyRepository.FindById(context.TODO(), strategyId)
if err != nil {
if !errors.Is(gorm.ErrRecordNotFound, err) {
return nil, err
}
} else {
upload = service.renderBoolToStr(strategy.Upload)
download = service.renderBoolToStr(strategy.Download)
_delete = service.renderBoolToStr(strategy.Delete)
rename = service.renderBoolToStr(strategy.Rename)
edit = service.renderBoolToStr(strategy.Edit)
_copy = service.renderBoolToStr(strategy.Copy)
paste = service.renderBoolToStr(strategy.Paste)
}
}
}
var storageId = ""
if nt.RDP == asset.Protocol {
attr, err := repository.AssetRepository.FindAssetAttrMapByAssetId(context.TODO(), assetId)
if err != nil {
return nil, err
}
if "true" == attr[guacamole.EnableDrive] {
fileSystem = "1"
storageId = attr[guacamole.DrivePath]
if storageId == "" {
storageId = user.ID
}
} else {
fileSystem = "0"
}
}
if fileSystem != "1" {
fileSystem = "0"
}
if upload != "1" {
upload = "0"
}
if download != "1" {
download = "0"
}
if _delete != "1" {
_delete = "0"
}
if rename != "1" {
rename = "0"
}
if edit != "1" {
edit = "0"
}
if _copy != "1" {
_copy = "0"
}
if paste != "1" {
paste = "0"
}
s := &model.Session{
ID: utils.UUID(),
AssetId: asset.ID,
Username: asset.Username,
Password: asset.Password,
PrivateKey: asset.PrivateKey,
Passphrase: asset.Passphrase,
Protocol: asset.Protocol,
IP: asset.IP,
Port: asset.Port,
Status: nt.NoConnect,
ClientIP: clientIp,
Mode: mode,
FileSystem: fileSystem,
Upload: upload,
Download: download,
Delete: _delete,
Rename: rename,
Edit: edit,
Copy: _copy,
Paste: paste,
StorageId: storageId,
AccessGatewayId: asset.AccessGatewayId,
Reviewed: false,
}
if nt.Anonymous != user.Type {
s.Creator = user.ID
}
if asset.AccountType == "credential" {
credential, err := repository.CredentialRepository.FindById(context.TODO(), asset.CredentialId)
if err != nil {
return nil, err
}
if credential.Type == nt.Custom {
s.Username = credential.Username
s.Password = credential.Password
} else {
s.Username = credential.Username
s.PrivateKey = credential.PrivateKey
s.Passphrase = credential.Passphrase
}
}
if err := repository.SessionRepository.Create(context.TODO(), s); err != nil {
return nil, err
}
if err := repository.AssetRepository.UpdateLastAccessTime(context.Background(), s.AssetId, common.NowJsonTime()); err != nil {
return nil, err
}
return s, nil
}
func (service sessionService) FixSshMode() error {
return repository.SessionRepository.UpdateMode(context.TODO())
}
+313
View File
@@ -0,0 +1,313 @@
package service
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path"
"strconv"
"strings"
"next-terminal/server/common"
"next-terminal/server/config"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
var StorageService = new(storageService)
type storageService struct {
}
func (service storageService) InitStorages() error {
users, err := repository.UserRepository.FindAll(context.TODO())
if err != nil {
return err
}
for i := range users {
userId := users[i].ID
_, err := repository.StorageRepository.FindByOwnerIdAndDefault(context.TODO(), userId, true)
if errors.Is(err, gorm.ErrRecordNotFound) {
err = service.CreateStorageByUser(context.TODO(), &users[i])
if err != nil {
return err
}
}
}
drivePath := service.GetBaseDrivePath()
storages, err := repository.StorageRepository.FindAll(context.TODO())
if err != nil {
return err
}
for i := 0; i < len(storages); i++ {
storage := storages[i]
// 判断是否为遗留的数据:磁盘空间在,但用户已删除
if storage.IsDefault {
var userExist = false
for j := range users {
if storage.ID == users[j].ID {
userExist = true
break
}
}
if !userExist {
if err := service.DeleteStorageById(context.TODO(), storage.ID, true); err != nil {
return err
}
}
}
storageDir := path.Join(drivePath, storage.ID)
if !utils.FileExists(storageDir) {
if err := os.MkdirAll(storageDir, os.ModePerm); err != nil {
return err
}
}
}
return nil
}
func (service storageService) CreateStorageByUser(c context.Context, user *model.User) error {
drivePath := service.GetBaseDrivePath()
var limitSize int64
property, err := repository.PropertyRepository.FindByName(c, "user-default-storage-size")
if err != nil {
return err
}
limitSize, err = strconv.ParseInt(property.Value, 10, 64)
if err != nil {
return err
}
limitSize = limitSize * 1024 * 1024
if limitSize < 0 {
limitSize = -1
}
storage := model.Storage{
ID: user.ID,
Name: user.Nickname + "的默认空间",
IsShare: false,
IsDefault: true,
LimitSize: limitSize,
Owner: user.ID,
Created: common.NowJsonTime(),
}
storageDir := path.Join(drivePath, storage.ID)
if err := os.MkdirAll(storageDir, os.ModePerm); err != nil {
return err
}
err = repository.StorageRepository.Create(c, &storage)
if err != nil {
_ = os.RemoveAll(storageDir)
return err
}
return nil
}
type File struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Mode string `json:"mode"`
IsLink bool `json:"isLink"`
ModTime common.JsonTime `json:"modTime"`
Size int64 `json:"size"`
}
func (service storageService) Ls(drivePath, remoteDir string) ([]File, error) {
fileInfos, err := ioutil.ReadDir(path.Join(drivePath, remoteDir))
if err != nil {
return nil, err
}
var files = make([]File, 0)
for i := range fileInfos {
file := File{
Name: fileInfos[i].Name(),
Path: path.Join(remoteDir, fileInfos[i].Name()),
IsDir: fileInfos[i].IsDir(),
Mode: fileInfos[i].Mode().String(),
IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink,
ModTime: common.NewJsonTime(fileInfos[i].ModTime()),
Size: fileInfos[i].Size(),
}
files = append(files, file)
}
return files, nil
}
func (service storageService) GetBaseDrivePath() string {
return config.GlobalCfg.Guacd.Drive
}
func (service storageService) DeleteStorageById(c context.Context, id string, force bool) error {
drivePath := service.GetBaseDrivePath()
storage, err := repository.StorageRepository.FindById(c, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
if !force && storage.IsDefault {
return errors.New("默认空间不能删除")
}
// 删除对应的本地目录
if err := os.RemoveAll(path.Join(drivePath, id)); err != nil {
return err
}
if err := repository.StorageRepository.DeleteById(c, id); err != nil {
return err
}
return nil
}
func (service storageService) StorageUpload(c echo.Context, file *multipart.FileHeader, storageId string) error {
drivePath := service.GetBaseDrivePath()
storage, _ := repository.StorageRepository.FindById(context.TODO(), storageId)
if storage.LimitSize > 0 {
dirSize, err := utils.DirSize(path.Join(drivePath, storageId))
if err != nil {
return err
}
if dirSize+file.Size > storage.LimitSize {
return errors.New("可用空间不足")
}
}
filename := file.Filename
src, err := file.Open()
if err != nil {
return err
}
remoteDir := c.QueryParam("dir")
remoteFile := path.Join(remoteDir, filename)
if strings.Contains(remoteDir, "../") {
return errors.New("非法请求 :(")
}
if strings.Contains(remoteFile, "../") {
return errors.New("非法请求 :(")
}
// 判断文件夹不存在时自动创建
dir := path.Join(path.Join(drivePath, storageId), remoteDir)
if !utils.FileExists(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
// Destination
dst, err := os.Create(path.Join(path.Join(drivePath, storageId), remoteFile))
if err != nil {
return err
}
defer dst.Close()
// Copy
if _, err = io.Copy(dst, src); err != nil {
return err
}
return nil
}
func (service storageService) StorageEdit(file string, fileContent string, storageId string) error {
drivePath := service.GetBaseDrivePath()
if strings.Contains(file, "../") {
return errors.New("非法请求 :(")
}
realFilePath := path.Join(path.Join(drivePath, storageId), file)
dstFile, err := os.OpenFile(realFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer dstFile.Close()
write := bufio.NewWriter(dstFile)
if _, err := write.WriteString(fileContent); err != nil {
return err
}
if err := write.Flush(); err != nil {
return err
}
return nil
}
func (service storageService) StorageDownload(c echo.Context, file, storageId string) error {
drivePath := service.GetBaseDrivePath()
if strings.Contains(file, "../") {
return errors.New("非法请求 :(")
}
// 获取带后缀的文件名称
filenameWithSuffix := path.Base(file)
p := path.Join(path.Join(drivePath, storageId), file)
//log.Infof("download %v", p)
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filenameWithSuffix))
c.Response().Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(c.Response(), c.Request(), p)
return nil
}
func (service storageService) StorageLs(remoteDir, storageId string) (error, []File) {
drivePath := service.GetBaseDrivePath()
if strings.Contains(remoteDir, "../") {
return errors.New("非法请求 :("), nil
}
files, err := service.Ls(path.Join(drivePath, storageId), remoteDir)
if err != nil {
return err, nil
}
return nil, files
}
func (service storageService) StorageMkDir(remoteDir, storageId string) error {
drivePath := service.GetBaseDrivePath()
if strings.Contains(remoteDir, "../") {
return errors.New("非法请求 :(")
}
if err := os.MkdirAll(path.Join(path.Join(drivePath, storageId), remoteDir), os.ModePerm); err != nil {
return err
}
return nil
}
func (service storageService) StorageRm(file, storageId string) error {
drivePath := service.GetBaseDrivePath()
if strings.Contains(file, "../") {
return errors.New("非法请求 :(")
}
if err := os.RemoveAll(path.Join(path.Join(drivePath, storageId), file)); err != nil {
return err
}
return nil
}
func (service storageService) StorageRename(oldName, newName, storageId string) error {
drivePath := service.GetBaseDrivePath()
if strings.Contains(oldName, "../") {
return errors.New("非法请求 :(")
}
if strings.Contains(newName, "../") {
return errors.New("非法请求 :(")
}
if err := os.Rename(path.Join(path.Join(drivePath, storageId), oldName), path.Join(path.Join(drivePath, storageId), newName)); err != nil {
return err
}
return nil
}
+29
View File
@@ -0,0 +1,29 @@
package service
import (
"context"
"next-terminal/server/common"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var StorageLogService = new(storageLogService)
type storageLogService struct {
baseService
}
func (s storageLogService) Save(ctx context.Context, assetId, sessionId, userId, action, filename string) error {
storageLog := &model.StorageLog{
ID: utils.UUID(),
AssetId: assetId,
SessionId: sessionId,
UserId: userId,
Action: action,
FileName: filename,
Created: common.NowJsonTime(),
}
return repository.StorageLogRepository.Create(ctx, storageLog)
}
+502
View File
@@ -0,0 +1,502 @@
package service
import (
"errors"
"fmt"
"next-terminal/server/common/nt"
"strings"
"next-terminal/server/branding"
"next-terminal/server/common"
"next-terminal/server/dto"
"next-terminal/server/env"
"next-terminal/server/global/cache"
"next-terminal/server/log"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"golang.org/x/net/context"
"gorm.io/gorm"
)
const SuperAdminID = `abcdefghijklmnopqrstuvwxyz`
var UserService = new(userService)
type userService struct {
baseService
}
func (service userService) InitUser() (err error) {
users, err := repository.UserRepository.FindAll(context.TODO())
if err != nil {
return err
}
if len(users) == 0 {
initPassword := "admin"
var pass []byte
if pass, err = utils.Encoder.Encode([]byte(initPassword)); err != nil {
return err
}
user := model.User{
ID: SuperAdminID,
Username: "admin",
Password: string(pass),
Nickname: "超级管理员",
Type: nt.TypeAdmin,
Created: common.NowJsonTime(),
Status: nt.StatusEnabled,
}
if err := repository.UserRepository.Create(context.TODO(), &user); err != nil {
return err
}
} else {
for i := range users {
// 修正默认用户类型为管理员
if users[i].Type == "" {
user := model.User{
Type: nt.TypeUser,
ID: users[i].ID,
}
if err := repository.UserRepository.Update(context.TODO(), &user); err != nil {
return err
}
}
if users[i].Type == nt.TypeAdmin {
roles, err := RoleService.GetRolesByUserId(users[i].ID)
if err != nil {
return err
}
if len(roles) == 0 {
users[i].Roles = []string{"system-administrator"}
if err := service.saveUserRoles(context.Background(), users[i]); err != nil {
return err
}
}
}
}
}
return nil
}
func (service userService) IsSuperAdmin(userId string) bool {
return SuperAdminID == userId
}
func (service userService) FixUserOnlineState() error {
// 修正用户登录状态
onlineUsers, err := repository.UserRepository.FindOnlineUsers(context.TODO())
if err != nil {
return err
}
if len(onlineUsers) > 0 {
for i := range onlineUsers {
logs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(context.TODO(), onlineUsers[i].Username)
if err != nil {
return err
}
if len(logs) == 0 {
if err := repository.UserRepository.UpdateOnlineByUsername(context.TODO(), onlineUsers[i].Username, false); err != nil {
return err
}
}
}
}
return nil
}
func (service userService) Logout(token string) {
cache.TokenManager.Delete(token)
}
func (service userService) LogoutByToken(token string) (err error) {
loginLog, err := repository.LoginLogRepository.FindById(context.TODO(), token)
if err != nil {
return err
}
loginLogForUpdate := &model.LoginLog{LogoutTime: common.NowJsonTime(), ID: token}
err = repository.LoginLogRepository.Update(context.TODO(), loginLogForUpdate)
if err != nil {
return err
}
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(context.TODO(), loginLog.Username)
if err != nil {
return err
}
if len(loginLogs) == 0 {
err = repository.UserRepository.UpdateOnlineByUsername(context.TODO(), loginLog.Username, false)
}
return err
}
func (service userService) LogoutById(c context.Context, id string) error {
user, err := repository.UserRepository.FindById(c, id)
if err != nil {
return err
}
username := user.Username
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(c, username)
if err != nil {
return err
}
for j := range loginLogs {
token := loginLogs[j].ID
service.Logout(token)
}
return nil
}
func (service userService) GetUserLoginToken(c context.Context, username string) ([]string, error) {
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogsByUsername(c, username)
if err != nil {
return nil, err
}
var tokens []string
for j := range loginLogs {
token := loginLogs[j].ID
tokens = append(tokens, token)
}
return tokens, nil
}
func (service userService) OnEvicted(token string, value interface{}) {
if strings.HasPrefix(token, "forever") {
} else {
err := service.LogoutByToken(token)
if err != nil && !errors.Is(gorm.ErrRecordNotFound, err) {
}
}
}
func (service userService) UpdateStatusById(id string, status string) error {
if nt.StatusDisabled == status {
// 将该用户下线
if err := service.LogoutById(context.TODO(), id); err != nil {
return err
}
}
u := model.User{
ID: id,
Status: status,
}
return repository.UserRepository.Update(context.TODO(), &u)
}
func (service userService) ReloadToken() error {
loginLogs, err := repository.LoginLogRepository.FindAliveLoginLogs(context.TODO())
if err != nil {
return err
}
for i := range loginLogs {
loginLog := loginLogs[i]
token := loginLog.ID
user, err := repository.UserRepository.FindByUsername(context.TODO(), loginLog.Username)
if err != nil {
if errors.Is(gorm.ErrRecordNotFound, err) {
_ = repository.LoginLogRepository.DeleteById(context.TODO(), token)
}
continue
}
authorization := dto.Authorization{
Token: token,
Type: nt.LoginToken,
Remember: loginLog.Remember,
User: &user,
}
if authorization.Remember {
// 记住登录有效期两周
cache.TokenManager.Set(token, authorization, cache.RememberMeExpiration)
} else {
cache.TokenManager.Set(token, authorization, cache.NotRememberExpiration)
}
log.Debug("重新加载用户授权Token", log.String("username", user.Nickname), log.String("token", token))
}
return nil
}
func (service userService) CreateUser(user model.User) (err error) {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
exist, err := repository.UserRepository.ExistByUsername(c, user.Username)
if err != nil {
return err
}
if exist {
return fmt.Errorf("username %s is already used", user.Username)
}
password := user.Password
var pass []byte
if pass, err = utils.Encoder.Encode([]byte(password)); err != nil {
return err
}
user.Password = string(pass)
user.ID = utils.UUID()
user.Created = common.NowJsonTime()
user.Status = nt.StatusEnabled
if err := repository.UserRepository.Create(c, &user); err != nil {
return err
}
if err := service.saveUserRoles(c, user); err != nil {
return err
}
if err := StorageService.CreateStorageByUser(c, &user); err != nil {
return err
}
if user.Mail != "" {
subject := fmt.Sprintf("%s 注册通知", branding.Name)
text := fmt.Sprintf(`您好,%s。
管理员为你开通了账户。
账号:%s
密码:%s
`, user.Username, user.Username, password)
go MailService.SendMail(user.Mail, subject, text)
}
return nil
})
}
func (service userService) saveUserRoles(c context.Context, user model.User) error {
for _, role := range user.Roles {
ref := &model.UserRoleRef{
ID: utils.UUID(),
UserId: user.ID,
RoleId: role,
}
if err := repository.UserRoleRefRepository.Create(c, ref); err != nil {
return err
}
}
return nil
}
func (service userService) DeleteUserById(userId string) error {
user, err := repository.UserRepository.FindById(context.TODO(), userId)
if err != nil {
return err
}
username := user.Username
// 下线该用户
loginTokens, err := service.GetUserLoginToken(context.TODO(), username)
if err != nil {
return err
}
err = env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
// 删除用户与用户组的关系
if err := repository.UserGroupMemberRepository.DeleteByUserId(c, userId); err != nil {
return err
}
// 删除用户与资产的关系
if err := repository.AuthorisedRepository.DeleteByUserId(c, userId); err != nil {
return err
}
// 删除用户的默认磁盘空间
if err := StorageService.DeleteStorageById(c, userId, true); err != nil {
return err
}
// 删除用户与角色的关系
if err := repository.UserRoleRefRepository.DeleteByUserId(c, user.ID); err != nil {
return err
}
// 删除用户
if err := repository.UserRepository.DeleteById(c, userId); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
for _, token := range loginTokens {
service.Logout(token)
}
return nil
}
func (service userService) DeleteLoginLogs(tokens []string) error {
if len(tokens) > 0 {
for _, token := range tokens {
// 手动触发用户退出登录
if err := service.LogoutByToken(token); err != nil {
return err
}
// 移除缓存中的token
service.Logout(token)
// 删除登录日志
if err := repository.LoginLogRepository.DeleteById(context.TODO(), token); err != nil {
return err
}
}
}
return nil
}
func (service userService) SaveLoginLog(clientIP, clientUserAgent string, username string, success, remember bool, id, reason string) error {
loginLog := model.LoginLog{
Username: username,
ClientIP: clientIP,
ClientUserAgent: clientUserAgent,
LoginTime: common.NowJsonTime(),
Reason: reason,
Remember: remember,
}
if success {
loginLog.State = "1"
loginLog.ID = id
} else {
loginLog.State = "0"
loginLog.ID = utils.LongUUID()
}
if err := repository.LoginLogRepository.Create(context.TODO(), &loginLog); err != nil {
return err
}
return nil
}
func (service userService) DeleteALlLdapUser(ctx context.Context) error {
return repository.UserRepository.DeleteBySource(ctx, nt.SourceLdap)
}
func (service userService) UpdateUser(id string, user model.User) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
ctx := service.Context(tx)
dbUser, err := repository.UserRepository.FindById(ctx, id)
if err != nil {
return err
}
if dbUser.Username != user.Username {
// 修改了登录账号
exist, err := repository.UserRepository.ExistByUsername(ctx, user.Username)
if err != nil {
return err
}
if exist {
return fmt.Errorf("username %s is already used", user.Username)
}
}
if err := repository.UserRoleRefRepository.DeleteByUserId(ctx, user.ID); err != nil {
return err
}
if err := service.saveUserRoles(ctx, user); err != nil {
return err
}
// 移除用户角色的缓存
cache.UserRolesManager.Delete(id)
// 保存用户部门
var deptIds []string
for _, dept := range user.Departments {
deptIds = append(deptIds, dept.ID)
}
if err := repository.UserRepository.SaveUserDepartments(ctx, id, deptIds); err != nil {
return err
}
return repository.UserRepository.Update(ctx, &user)
})
}
func (service userService) FindById(id string) (*model.User, error) {
item, err := repository.UserRepository.FindById(context.TODO(), id)
if err != nil {
return nil, err
}
roles, err := RoleService.GetRolesByUserId(id)
if err != nil {
return nil, err
}
item.Roles = roles
// 加载用户部门
departments, err := repository.UserRepository.FindDepartmentsByUserId(context.TODO(), id)
if err != nil {
return nil, err
}
item.Departments = departments
return &item, nil
}
func (service userService) ResetTotp(ids []string) error {
return service.Transaction(context.Background(), func(ctx context.Context) error {
for _, id := range ids {
u := &model.User{
TOTPSecret: "-",
ID: id,
}
if err := repository.UserRepository.Update(ctx, u); err != nil {
return err
}
}
return nil
})
}
func (service userService) ChangePassword(ids []string, password string) error {
passwd, err := utils.Encoder.Encode([]byte(password))
if err != nil {
return err
}
return service.Transaction(context.Background(), func(ctx context.Context) error {
for _, id := range ids {
u := &model.User{
Password: string(passwd),
ID: id,
}
if err := repository.UserRepository.Update(ctx, u); err != nil {
return err
}
user, err := repository.UserRepository.FindById(ctx, id)
if err != nil {
return err
}
if user.Mail != "" {
subject := "密码修改通知"
text := fmt.Sprintf(`您好,%s。
管理员已将你的密码修改为:%s。
`, user.Username, password)
go MailService.SendMail(user.Mail, subject, text)
}
}
return nil
})
}
+121
View File
@@ -0,0 +1,121 @@
package service
import (
"context"
"next-terminal/server/common"
"next-terminal/server/common/nt"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
var UserGroupService = new(userGroupService)
type userGroupService struct {
baseService
}
func (service userGroupService) DeleteById(userGroupId string) error {
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
// 删除用户组
if err := repository.UserGroupRepository.DeleteById(c, userGroupId); err != nil {
return err
}
// 删除用户组与用户的关系
if err := repository.UserGroupMemberRepository.DeleteByUserGroupId(c, userGroupId); err != nil {
return err
}
// 删除用户组与资产的关系
if err := repository.AuthorisedRepository.DeleteByUserGroupId(c, userGroupId); err != nil {
return err
}
return nil
})
}
func (service userGroupService) Create(ctx context.Context, name string, members []string) (model.UserGroup, error) {
exist, err := repository.UserGroupRepository.ExistByName(ctx, name)
if err != nil {
return model.UserGroup{}, err
}
if exist {
return model.UserGroup{}, nt.ErrNameAlreadyUsed
}
userGroupId := utils.UUID()
userGroup := model.UserGroup{
ID: userGroupId,
Created: common.NowJsonTime(),
Name: name,
}
return userGroup, service.Transaction(ctx, func(ctx context.Context) error {
if err := repository.UserGroupRepository.Create(ctx, &userGroup); err != nil {
return err
}
if len(members) > 0 {
for _, member := range members {
userGroupMember := model.UserGroupMember{
ID: utils.Sign([]string{userGroupId, member}),
UserId: member,
UserGroupId: userGroupId,
}
if err := repository.UserGroupMemberRepository.Create(ctx, &userGroupMember); err != nil {
return err
}
}
}
return nil
})
}
func (service userGroupService) Update(userGroupId string, name string, members []string) (err error) {
dbUserGroup, err := repository.UserGroupRepository.FindById(context.TODO(), userGroupId)
if err != nil {
return err
}
if dbUserGroup.Name != name {
// 修改了名称
exist, err := repository.UserGroupRepository.ExistByName(context.TODO(), name)
if err != nil {
return err
}
if exist {
return nt.ErrNameAlreadyUsed
}
}
return env.GetDB().Transaction(func(tx *gorm.DB) error {
c := service.Context(tx)
userGroup := model.UserGroup{
ID: userGroupId,
Name: name,
}
if err := repository.UserGroupRepository.Update(c, &userGroup); err != nil {
return err
}
if err := repository.UserGroupMemberRepository.DeleteByUserGroupId(c, userGroupId); err != nil {
return err
}
if len(members) > 0 {
for _, member := range members {
userGroupMember := model.UserGroupMember{
ID: utils.Sign([]string{userGroupId, member}),
UserId: member,
UserGroupId: userGroupId,
}
if err := repository.UserGroupMemberRepository.Create(c, &userGroupMember); err != nil {
return err
}
}
}
return nil
})
}
+84
View File
@@ -0,0 +1,84 @@
package service
import (
"context"
"next-terminal/server/common/sets"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
)
var WorkerService = &workerService{}
type workerService struct {
}
func (s *workerService) FindMyAssetPaging(pageIndex, pageSize int, name, protocol, tags string, userId string, order, field string) (o []model.AssetForPage, total int64, err error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
return nil, 0, err
}
items, total, err := repository.AssetRepository.FindMyAssets(context.Background(), pageIndex, pageSize, name, protocol, tags, assetIdList, order, field)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
func (s *workerService) FindMyAsset(name, protocol, tags string, userId string, order, field string) (o []model.AssetForPage, err error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
return nil, err
}
items, _, err := repository.AssetRepository.FindMyAssets(context.Background(), 1, 1000, name, protocol, tags, assetIdList, order, field)
if err != nil {
return nil, err
}
return items, nil
}
func (s *workerService) FindMyAssetTags(ctx context.Context, userId string) ([]string, error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
return nil, err
}
tags, err := repository.AssetRepository.FindMyAssetTags(ctx, assetIdList)
return tags, err
}
func (s *workerService) getAssetIdListByUserId(userId string) ([]string, error) {
set := sets.NewStringSet()
authorisedByUser, err := repository.AuthorisedRepository.FindByUserId(context.Background(), userId)
if err != nil {
return nil, err
}
for _, authorised := range authorisedByUser {
set.Add(authorised.AssetId)
}
userGroupIds, err := repository.UserGroupMemberRepository.FindUserGroupIdsByUserId(context.Background(), userId)
if err != nil {
return nil, err
}
authorisedByUserGroup, err := repository.AuthorisedRepository.FindByUserGroupIdIn(context.Background(), userGroupIds)
if err != nil {
return nil, err
}
for _, authorised := range authorisedByUserGroup {
set.Add(authorised.AssetId)
}
return set.ToArray(), nil
}
func (s *workerService) CheckPermission(assetId, userId string) (bool, error) {
assetIdList, err := s.getAssetIdListByUserId(userId)
if err != nil {
return false, err
}
return utils.Contains(assetIdList, assetId), nil
}