diff --git a/internal/eventlog/events.go b/internal/eventlog/events.go index cc9cabc8..6cb282a9 100644 --- a/internal/eventlog/events.go +++ b/internal/eventlog/events.go @@ -7,34 +7,86 @@ import ( ) const ( - tailnetCreated = "ionscale.tailnet.created" - tailnetDeleted = "ionscale.tailnet.deleted" - nodeCreated = "ionscale.node.created" + tailnetCreated = "ionscale.tailnet.create" + tailnetIamUpdated = "ionscale.tailnet.iam.update" + tailnetAclUpdated = "ionscale.tailnet.acl.update" + tailnetDNSConfigUpdated = "ionscale.tailnet.dns_config.update" + nodeCreated = "ionscale.node.create" ) -func TailnetCreated(tailnet *domain.Tailnet, actor *domain.User) cloudevents.Event { - data := &EventData{ +func TailnetCreated(tailnet *domain.Tailnet, actor ActorOpts) cloudevents.Event { + data := &EventData[any]{ Tailnet: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, Target: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, - Actor: system, + Actor: actor(), } - if actor != nil { - data.Actor = Actor{ID: idToStr(actor.ID), Name: actor.Name} + event := cloudevents.NewEvent() + event.SetType(tailnetCreated) + _ = event.SetData(cloudevents.ApplicationJSON, data) + + return event +} + +func TailnetIAMUpdated(tailnet *domain.Tailnet, old *domain.IAMPolicy, actor ActorOpts) cloudevents.Event { + data := &EventData[*domain.IAMPolicy]{ + Tailnet: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Target: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Actor: actor(), + Attr: &Attr[*domain.IAMPolicy]{ + New: &tailnet.IAMPolicy, + Old: old, + }, } event := cloudevents.NewEvent() - event.SetType(tailnetCreated) + event.SetType(tailnetIamUpdated) + _ = event.SetData(cloudevents.ApplicationJSON, data) + + return event +} + +func TailnetACLUpdated(tailnet *domain.Tailnet, old *domain.ACLPolicy, actor ActorOpts) cloudevents.Event { + data := &EventData[*domain.ACLPolicy]{ + Tailnet: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Target: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Actor: actor(), + Attr: &Attr[*domain.ACLPolicy]{ + New: &tailnet.ACLPolicy, + Old: old, + }, + } + + event := cloudevents.NewEvent() + event.SetType(tailnetAclUpdated) _ = event.SetData(cloudevents.ApplicationJSON, data) return event } -func MachineCreated(machine *domain.Machine, actor *domain.User) cloudevents.Event { - data := &EventData{ +func TailnetDNSConfigUpdated(tailnet *domain.Tailnet, old *domain.DNSConfig, actor ActorOpts) cloudevents.Event { + data := &EventData[*domain.DNSConfig]{ + Tailnet: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Target: &Target{ID: idToStr(tailnet.ID), Name: tailnet.Name}, + Actor: actor(), + Attr: &Attr[*domain.DNSConfig]{ + New: &tailnet.DNSConfig, + Old: old, + }, + } + + event := cloudevents.NewEvent() + event.SetType(tailnetDNSConfigUpdated) + _ = event.SetData(cloudevents.ApplicationJSON, data) + + return event +} + +func MachineCreated(machine *domain.Machine, actor ActorOpts) cloudevents.Event { + data := &EventData[any]{ Tailnet: &Target{ID: idToStr(machine.Tailnet.ID), Name: machine.Tailnet.Name}, - Target: &Target{ID: idToStr(machine.ID), Name: machine.CompleteName(), Addresses: machine.IPs()}, - Actor: UserToActor(actor), + Target: &Target{ID: idToStr(machine.ID), Name: machine.CompleteName()}, + Actor: actor(), } event := cloudevents.NewEvent() @@ -44,29 +96,39 @@ func MachineCreated(machine *domain.Machine, actor *domain.User) cloudevents.Eve return event } -func UserToActor(actor *domain.User) Actor { - if actor == nil { - return system +type ActorOpts func() Actor + +func User(u *domain.User) ActorOpts { + if u == nil { + return SystemAdmin() } - switch actor.UserType { + switch u.UserType { case domain.UserTypePerson: - return Actor{ID: idToStr(actor.ID), Name: actor.Name} + return func() Actor { + return Actor{ID: idToStr(u.ID), Name: u.Name} + } default: - return system + return SystemAdmin() + } +} + +func SystemAdmin() ActorOpts { + return func() Actor { + return Actor{ID: "", Name: "system admin"} } } -type EventData struct { - Tailnet *Target `json:"tailnet,omitempty"` - Target *Target `json:"target,omitempty"` - Actor Actor `json:"actor"` +type EventData[T any] struct { + Tailnet *Target `json:"tailnet,omitempty"` + Target *Target `json:"target,omitempty"` + Attr *Attr[T] `json:"attr,omitempty"` + Actor Actor `json:"actor"` } type Target struct { - ID string `json:"id"` - Name string `json:"name"` - Addresses []string `json:"addresses,omitempty"` + ID string `json:"id"` + Name string `json:"name"` } type Actor struct { @@ -74,8 +136,11 @@ type Actor struct { Name string `json:"name"` } +type Attr[T any] struct { + New T `json:"new"` + Old T `json:"old,omitempty"` +} + func idToStr(id uint64) string { return big.NewInt(int64(id)).Text(10) } - -var system = Actor{ID: "", Name: "ionscale system"} diff --git a/internal/eventlog/global.go b/internal/eventlog/global.go index e0a774a0..57ee4f4c 100644 --- a/internal/eventlog/global.go +++ b/internal/eventlog/global.go @@ -99,7 +99,7 @@ func Configure(c *config.Config) error { _globalMu.Lock() defer _globalMu.Unlock() _globalE = &eventer{ - source: c.ServerUrl, + source: c.WebPublicUrl.String(), sinks: sinks, } diff --git a/internal/handlers/authentication.go b/internal/handlers/authentication.go index 8fd40220..65c17d56 100644 --- a/internal/handlers/authentication.go +++ b/internal/handlers/authentication.go @@ -509,7 +509,7 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form m.IPv4 = domain.IP{Addr: ipv4} m.IPv6 = domain.IP{Addr: ipv6} - events = append(events, eventlog.MachineCreated(m, user)) + events = append(events, eventlog.MachineCreated(m, eventlog.User(user))) } else { registeredTags := tags advertisedTags := domain.SanitizeTags(req.Hostinfo.RequestTags) diff --git a/internal/handlers/registration.go b/internal/handlers/registration.go index 9ed0b209..bb6d3957 100644 --- a/internal/handlers/registration.go +++ b/internal/handlers/registration.go @@ -222,7 +222,7 @@ func (h *RegistrationHandlers) authenticateMachineWithAuthKey(c echo.Context, ma m.IPv4 = domain.IP{Addr: ipv4} m.IPv6 = domain.IP{Addr: ipv6} - events = append(events, eventlog.MachineCreated(m, &user)) + events = append(events, eventlog.MachineCreated(m, eventlog.User(&user))) } else { sanitizeHostname := dnsname.SanitizeHostname(req.Hostinfo.Hostname) if m.Name != sanitizeHostname { diff --git a/internal/service/acl.go b/internal/service/acl.go index f1e49c91..5eb40d8c 100644 --- a/internal/service/acl.go +++ b/internal/service/acl.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bufbuild/connect-go" "github.com/jsiebens/ionscale/internal/domain" + "github.com/jsiebens/ionscale/internal/eventlog" "github.com/jsiebens/ionscale/internal/mapping" api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1" ) @@ -60,6 +61,7 @@ func (s *Service) SetACLPolicy(ctx context.Context, req *connect.Request[api.Set return nil, logError(err) } + eventlog.Send(ctx, eventlog.TailnetACLUpdated(tailnet, &oldPolicy, eventlog.User(principal.User))) s.sessionManager.NotifyAll(tailnet.ID) return connect.NewResponse(&api.SetACLPolicyResponse{}), nil diff --git a/internal/service/dns.go b/internal/service/dns.go index 3350753d..cb1fb371 100644 --- a/internal/service/dns.go +++ b/internal/service/dns.go @@ -6,6 +6,7 @@ import ( "github.com/bufbuild/connect-go" "github.com/jsiebens/ionscale/internal/config" "github.com/jsiebens/ionscale/internal/domain" + "github.com/jsiebens/ionscale/internal/eventlog" api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1" ) @@ -66,6 +67,7 @@ func (s *Service) SetDNSConfig(ctx context.Context, req *connect.Request[api.Set return nil, logError(err) } + eventlog.Send(ctx, eventlog.TailnetDNSConfigUpdated(tailnet, &oldConfig, eventlog.User(principal.User))) s.sessionManager.NotifyAll(tailnet.ID) return connect.NewResponse(&api.SetDNSConfigResponse{Config: domainDNSConfigToApiDNSConfig(tailnet)}), nil diff --git a/internal/service/iam.go b/internal/service/iam.go index 8f742353..e481cdd2 100644 --- a/internal/service/iam.go +++ b/internal/service/iam.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bufbuild/connect-go" "github.com/jsiebens/ionscale/internal/domain" + "github.com/jsiebens/ionscale/internal/eventlog" api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1" ) @@ -68,6 +69,8 @@ func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.Set return nil, logError(err) } + eventlog.Send(ctx, eventlog.TailnetIAMUpdated(tailnet, &oldPolicy, eventlog.User(principal.User))) + return connect.NewResponse(&api.SetIAMPolicyResponse{}), nil } diff --git a/internal/service/tailnet.go b/internal/service/tailnet.go index 9ec6e142..c554ad37 100644 --- a/internal/service/tailnet.go +++ b/internal/service/tailnet.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/bufbuild/connect-go" + cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/jsiebens/ionscale/internal/domain" "github.com/jsiebens/ionscale/internal/eventlog" "github.com/jsiebens/ionscale/internal/mapping" @@ -97,7 +98,12 @@ func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.Cr return nil, logError(err) } - eventlog.Send(ctx, eventlog.TailnetCreated(tailnet, principal.User)) + eventlog.Send(ctx, + eventlog.TailnetCreated(tailnet, eventlog.User(principal.User)), + eventlog.TailnetIAMUpdated(tailnet, nil, eventlog.User(principal.User)), + eventlog.TailnetACLUpdated(tailnet, nil, eventlog.User(principal.User)), + eventlog.TailnetDNSConfigUpdated(tailnet, nil, eventlog.User(principal.User)), + ) resp := &api.CreateTailnetResponse{Tailnet: t} @@ -119,26 +125,48 @@ func (s *Service) UpdateTailnet(ctx context.Context, req *connect.Request[api.Up return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("tailnet not found")) } + events := make([]cloudevents.Event, 0) + if req.Msg.IamPolicy != nil { if err := validateIamPolicy(req.Msg.IamPolicy); err != nil { return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invalid iam policy: %w", err)) } - tailnet.IAMPolicy = domain.IAMPolicy{} - if err := mapping.CopyViaJson(req.Msg.IamPolicy, &tailnet.IAMPolicy); err != nil { + oldPolicy := tailnet.IAMPolicy + var newPolicy domain.IAMPolicy + + if err := mapping.CopyViaJson(req.Msg.IamPolicy, &newPolicy); err != nil { return nil, logError(err) } + + if !oldPolicy.Equal(&newPolicy) { + tailnet.IAMPolicy = newPolicy + events = append(events, eventlog.TailnetIAMUpdated(tailnet, &oldPolicy, eventlog.User(principal.User))) + } } if req.Msg.AclPolicy != nil { - tailnet.ACLPolicy = domain.ACLPolicy{} - if err := mapping.CopyViaJson(req.Msg.AclPolicy, &tailnet.ACLPolicy); err != nil { + oldPolicy := tailnet.ACLPolicy + var newPolicy domain.ACLPolicy + + if err := mapping.CopyViaJson(req.Msg.AclPolicy, &newPolicy); err != nil { return nil, logError(err) } + + if !oldPolicy.Equal(&newPolicy) { + tailnet.ACLPolicy = newPolicy + events = append(events, eventlog.TailnetACLUpdated(tailnet, &oldPolicy, eventlog.User(principal.User))) + } } if req.Msg.DnsConfig != nil { - tailnet.DNSConfig = apiDNSConfigToDomainDNSConfig(req.Msg.DnsConfig) + oldConfig := tailnet.DNSConfig + newConfig := apiDNSConfigToDomainDNSConfig(req.Msg.DnsConfig) + + if !oldConfig.Equal(&newConfig) { + tailnet.DNSConfig = newConfig + events = append(events, eventlog.TailnetDNSConfigUpdated(tailnet, &oldConfig, eventlog.User(principal.User))) + } } tailnet.ServiceCollectionEnabled = req.Msg.ServiceCollectionEnabled @@ -150,6 +178,7 @@ func (s *Service) UpdateTailnet(ctx context.Context, req *connect.Request[api.Up return nil, logError(err) } + eventlog.Send(ctx, events...) s.sessionManager.NotifyAll(tailnet.ID) t, err := domainTailnetToApiTailnet(tailnet)