You've already forked ionscale
mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-04-05 12:32:58 +01:00
feat: add support for ssh check periods
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func m202312271200_account_last_authenticated() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "202312271200",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
type Account struct {
|
||||
LastAuthenticated *time.Time
|
||||
}
|
||||
|
||||
return db.AutoMigrate(
|
||||
&Account{},
|
||||
)
|
||||
},
|
||||
Rollback: nil,
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ func Migrations() []*gormigrate.Migration {
|
||||
m202211031100_add_authorized_column(),
|
||||
m202212201300_add_user_id_column(),
|
||||
m202212270800_machine_indeces(),
|
||||
m202312271200_account_last_authenticated(),
|
||||
}
|
||||
return migrations
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"errors"
|
||||
"github.com/jsiebens/ionscale/internal/util"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
ExternalID string
|
||||
LoginName string
|
||||
ID uint64 `gorm:"primary_key"`
|
||||
ExternalID string
|
||||
LoginName string
|
||||
LastAuthenticated *time.Time
|
||||
}
|
||||
|
||||
func (r *repository) GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error) {
|
||||
@@ -43,3 +45,17 @@ func (r *repository) GetAccount(ctx context.Context, id uint64) (*Account, error
|
||||
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (r *repository) SetAccountLastAuthenticated(ctx context.Context, accountID uint64) error {
|
||||
now := time.Now().UTC()
|
||||
tx := r.withContext(ctx).
|
||||
Model(Account{}).
|
||||
Where("id = ?", accountID).
|
||||
Updates(map[string]interface{}{"last_authenticated": &now})
|
||||
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -41,10 +41,11 @@ type ACL struct {
|
||||
}
|
||||
|
||||
type SSHRule struct {
|
||||
Action string `json:"action"`
|
||||
Src []string `json:"src"`
|
||||
Dst []string `json:"dst"`
|
||||
Users []string `json:"users"`
|
||||
Action string `json:"action"`
|
||||
Src []string `json:"src"`
|
||||
Dst []string `json:"dst"`
|
||||
Users []string `json:"users"`
|
||||
CheckPeriod string `json:"checkPeriod,omitempty"`
|
||||
}
|
||||
|
||||
func DefaultACLPolicy() ACLPolicy {
|
||||
|
||||
@@ -39,12 +39,18 @@ func (a ACLPolicy) BuildSSHPolicy(srcs []Machine, dst *Machine) *tailcfg.SSHPoli
|
||||
AllowLocalPortForwarding: true,
|
||||
}
|
||||
|
||||
if rule.Action == "check" {
|
||||
if rule.Action == "check" && rule.CheckPeriod == "" {
|
||||
action = &tailcfg.SSHAction{
|
||||
HoldAndDelegate: "https://unused/machine/ssh/action/$SRC_NODE_ID/to/$DST_NODE_ID",
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Action == "check" && rule.CheckPeriod != "" {
|
||||
action = &tailcfg.SSHAction{
|
||||
HoldAndDelegate: "https://unused/machine/ssh/action/$SRC_NODE_ID/to/$DST_NODE_ID/" + rule.CheckPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
selfUsers, otherUsers := a.expandSSHDstToSSHUsers(dst, rule)
|
||||
|
||||
if len(selfUsers) != 0 {
|
||||
|
||||
@@ -357,7 +357,7 @@ func (r *repository) DeleteMachine(ctx context.Context, id uint64) (bool, error)
|
||||
|
||||
func (r *repository) GetMachine(ctx context.Context, machineID uint64) (*Machine, error) {
|
||||
var m Machine
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").Take(&m, machineID)
|
||||
tx := r.withContext(ctx).Preload("Tailnet").Preload("User").Preload("User.Account").Take(&m, machineID)
|
||||
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
@@ -458,9 +458,10 @@ func (r *repository) ListMachineByTailnet(ctx context.Context, tailnetID uint64)
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("User").
|
||||
Where("tailnet_id = ?", tailnetID).
|
||||
Order("name asc, name_idx asc").
|
||||
Joins("User").
|
||||
Joins("User.Account").
|
||||
Where("machines.tailnet_id = ?", tailnetID).
|
||||
Order("machines.name asc, machines.name_idx asc").
|
||||
Find(&machines)
|
||||
|
||||
if tx.Error != nil {
|
||||
@@ -475,9 +476,10 @@ func (r *repository) ListMachinePeers(ctx context.Context, tailnetID uint64, key
|
||||
|
||||
tx := r.withContext(ctx).
|
||||
Preload("Tailnet").
|
||||
Preload("User").
|
||||
Where("tailnet_id = ? AND machine_key <> ?", tailnetID, key).
|
||||
Order("id asc").
|
||||
Joins("User").
|
||||
Joins("User.Account").
|
||||
Where("machines.tailnet_id = ? AND machines.machine_key <> ?", tailnetID, key).
|
||||
Order("machines.id asc").
|
||||
Find(&machines)
|
||||
|
||||
if tx.Error != nil {
|
||||
|
||||
@@ -23,6 +23,7 @@ type Repository interface {
|
||||
|
||||
GetAccount(ctx context.Context, accountID uint64) (*Account, error)
|
||||
GetOrCreateAccount(ctx context.Context, externalID, loginName string) (*Account, bool, error)
|
||||
SetAccountLastAuthenticated(ctx context.Context, accountID uint64) error
|
||||
|
||||
SaveTailnet(ctx context.Context, tailnet *Tailnet) error
|
||||
GetTailnet(ctx context.Context, id uint64) (*Tailnet, error)
|
||||
|
||||
@@ -149,6 +149,10 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if err := h.repository.SetAccountLastAuthenticated(ctx, account.ID); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if state.Flow == "s" {
|
||||
sshActionReq, err := h.repository.GetSSHActionRequest(ctx, state.Key)
|
||||
if err != nil || sshActionReq == nil {
|
||||
|
||||
@@ -29,6 +29,7 @@ type SSHActionHandlers struct {
|
||||
type sshActionRequestData struct {
|
||||
SrcMachineID uint64 `param:"src_machine_id"`
|
||||
DstMachineID uint64 `param:"dst_machine_id"`
|
||||
CheckPeriod string `param:"check_period"`
|
||||
}
|
||||
|
||||
func (h *SSHActionHandlers) StartAuth(c echo.Context) error {
|
||||
@@ -44,6 +45,32 @@ func (h *SSHActionHandlers) StartAuth(c echo.Context) error {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if data.CheckPeriod != "" {
|
||||
checkPeriod, err := time.ParseDuration(data.CheckPeriod)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
machine, err := h.repository.GetMachine(ctx, data.SrcMachineID)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if machine.User.Account != nil && machine.User.Account.LastAuthenticated != nil {
|
||||
sinceLastAuthentication := time.Since(*machine.User.Account.LastAuthenticated)
|
||||
|
||||
if sinceLastAuthentication < checkPeriod {
|
||||
resp := &tailcfg.SSHAction{
|
||||
Accept: true,
|
||||
AllowAgentForwarding: true,
|
||||
AllowLocalPortForwarding: true,
|
||||
}
|
||||
|
||||
return binder.WriteResponse(c, http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key := util.RandStringBytes(8)
|
||||
request := &domain.SSHActionRequest{
|
||||
Key: key,
|
||||
|
||||
@@ -164,9 +164,10 @@ func ToNode(m *domain.Machine, tailnet *domain.Tailnet, taggedDevicesUser *domai
|
||||
sanitizedTailnetName := domain.SanitizeTailnetName(m.Tailnet.Name)
|
||||
|
||||
hostInfo := tailcfg.Hostinfo{
|
||||
OS: hostinfo.OS,
|
||||
Hostname: hostinfo.Hostname,
|
||||
Services: filterServices(hostinfo.Services),
|
||||
OS: hostinfo.OS,
|
||||
Hostname: hostinfo.Hostname,
|
||||
Services: filterServices(hostinfo.Services),
|
||||
SSH_HostKeys: hostinfo.SSH_HostKeys,
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
|
||||
@@ -122,6 +122,7 @@ func Start(c *config.Config) error {
|
||||
e.POST("/machine/set-dns", dnsHandlers.SetDNS)
|
||||
e.POST("/machine/id-token", idTokenHandlers.FetchToken)
|
||||
e.GET("/machine/ssh/action/:src_machine_id/to/:dst_machine_id", sshActionHandlers.StartAuth)
|
||||
e.GET("/machine/ssh/action/:src_machine_id/to/:dst_machine_id/:check_period", sshActionHandlers.StartAuth)
|
||||
e.GET("/machine/ssh/action/check/:key", sshActionHandlers.CheckAuth)
|
||||
|
||||
return e
|
||||
|
||||
@@ -418,10 +418,11 @@ type SSHRule struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
|
||||
Src []string `protobuf:"bytes,2,rep,name=src,proto3" json:"src,omitempty"`
|
||||
Dst []string `protobuf:"bytes,3,rep,name=dst,proto3" json:"dst,omitempty"`
|
||||
Users []string `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
|
||||
Action string `protobuf:"bytes,1,opt,name=action,proto3" json:"action,omitempty"`
|
||||
Src []string `protobuf:"bytes,2,rep,name=src,proto3" json:"src,omitempty"`
|
||||
Dst []string `protobuf:"bytes,3,rep,name=dst,proto3" json:"dst,omitempty"`
|
||||
Users []string `protobuf:"bytes,4,rep,name=users,proto3" json:"users,omitempty"`
|
||||
Checkperiod string `protobuf:"bytes,5,opt,name=checkperiod,proto3" json:"checkperiod,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SSHRule) Reset() {
|
||||
@@ -484,6 +485,13 @@ func (x *SSHRule) GetUsers() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SSHRule) GetCheckperiod() string {
|
||||
if x != nil {
|
||||
return x.Checkperiod
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_ionscale_v1_acl_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_ionscale_v1_acl_proto_rawDesc = []byte{
|
||||
@@ -561,17 +569,19 @@ var file_ionscale_v1_acl_proto_rawDesc = []byte{
|
||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
|
||||
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
|
||||
0x22, 0x5b, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61,
|
||||
0x22, 0x7d, 0x0a, 0x07, 0x53, 0x53, 0x48, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x63, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x72, 0x63, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x03, 0x73, 0x72, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x73, 0x74, 0x18, 0x03, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x03, 0x64, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73,
|
||||
0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x42, 0x3d, 0x5a,
|
||||
0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x73, 0x69, 0x65,
|
||||
0x62, 0x65, 0x6e, 0x73, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x70, 0x6b,
|
||||
0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76,
|
||||
0x31, 0x3b, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x33,
|
||||
0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x12, 0x20, 0x0a,
|
||||
0x0b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x42,
|
||||
0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x73,
|
||||
0x69, 0x65, 0x62, 0x65, 0x6e, 0x73, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f,
|
||||
0x70, 0x6b, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65,
|
||||
0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x76, 0x31, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -46,4 +46,5 @@ message SSHRule {
|
||||
repeated string src = 2;
|
||||
repeated string dst = 3;
|
||||
repeated string users = 4;
|
||||
string checkperiod = 5;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user