diff --git a/internal/database/migration/m202312271200_account_last_authenticated.go b/internal/database/migration/m202312271200_account_last_authenticated.go new file mode 100644 index 0000000..6406b61 --- /dev/null +++ b/internal/database/migration/m202312271200_account_last_authenticated.go @@ -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, + } +} diff --git a/internal/database/migration/migrations.go b/internal/database/migration/migrations.go index 6f6f3dd..ef80d79 100644 --- a/internal/database/migration/migrations.go +++ b/internal/database/migration/migrations.go @@ -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 } diff --git a/internal/domain/account.go b/internal/domain/account.go index 81d801d..b591177 100644 --- a/internal/domain/account.go +++ b/internal/domain/account.go @@ -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 +} diff --git a/internal/domain/acl.go b/internal/domain/acl.go index 3c7d9e9..2f22d0d 100644 --- a/internal/domain/acl.go +++ b/internal/domain/acl.go @@ -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 { diff --git a/internal/domain/acl_ssh_policy.go b/internal/domain/acl_ssh_policy.go index bf04992..ebf1400 100644 --- a/internal/domain/acl_ssh_policy.go +++ b/internal/domain/acl_ssh_policy.go @@ -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 { diff --git a/internal/domain/machine.go b/internal/domain/machine.go index 7b69e1f..d39b12e 100644 --- a/internal/domain/machine.go +++ b/internal/domain/machine.go @@ -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 { diff --git a/internal/domain/repository.go b/internal/domain/repository.go index cd5bbc3..0f4d4be 100644 --- a/internal/domain/repository.go +++ b/internal/domain/repository.go @@ -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) diff --git a/internal/handlers/authentication.go b/internal/handlers/authentication.go index 2c804d3..7bf59d2 100644 --- a/internal/handlers/authentication.go +++ b/internal/handlers/authentication.go @@ -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 { diff --git a/internal/handlers/ssh_action.go b/internal/handlers/ssh_action.go index dd049ff..333e097 100644 --- a/internal/handlers/ssh_action.go +++ b/internal/handlers/ssh_action.go @@ -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, diff --git a/internal/mapping/mapping.go b/internal/mapping/mapping.go index 97637cf..313330b 100644 --- a/internal/mapping/mapping.go +++ b/internal/mapping/mapping.go @@ -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{ diff --git a/internal/server/server.go b/internal/server/server.go index c91e675..60358d5 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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 diff --git a/pkg/gen/ionscale/v1/acl.pb.go b/pkg/gen/ionscale/v1/acl.pb.go index 6930ab6..42c6f0f 100644 --- a/pkg/gen/ionscale/v1/acl.pb.go +++ b/pkg/gen/ionscale/v1/acl.pb.go @@ -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 ( diff --git a/proto/ionscale/v1/acl.proto b/proto/ionscale/v1/acl.proto index 1648b85..73ecfd3 100644 --- a/proto/ionscale/v1/acl.proto +++ b/proto/ionscale/v1/acl.proto @@ -46,4 +46,5 @@ message SSHRule { repeated string src = 2; repeated string dst = 3; repeated string users = 4; + string checkperiod = 5; }