NET-2014: Audit Logging (#3455)

* feat: api access tokens

* revoke all user tokens

* redefine access token api routes, add auto egress option to enrollment keys

* add server settings apis, add db table for settigs

* handle server settings updates

* switch to using settings from DB

* fix sever settings migration

* revet force migration for settings

* fix server settings database write

* egress model

* fix revoked tokens to be unauthorized

* update egress model

* remove unused functions

* convert access token to sql schema

* switch access token to sql schema

* fix merge conflicts

* fix server settings types

* bypass basic auth setting for super admin

* add TODO comment

* setup api handlers for egress revamp

* use single DB, fix update nat boolean field

* extend validaiton checks for egress ranges

* add migration to convert to new egress model

* fix panic interface conversion

* publish peer update on settings update

* revoke token generated by an user

* add user token creation restriction by user role

* add forbidden check for access token creation

* revoke user token when group or role is changed

* add default group to admin users on update

* chore(go): import style changes from migration branch;

1. Singular file names for table schema.
2. No table name method.
3. Use .Model instead of .Table.
4. No unnecessary tagging.

* remove nat check on egress gateway request

* Revert "remove nat check on egress gateway request"

This reverts commit 0aff12a189.

* remove nat check on egress gateway request

* feat(go): add db middleware;

* feat(go): restore method;

* feat(go): add user access token schema;

* add inet gw status to egress model

* fetch node ids in the tag, add inet gw info clients

* add inet gw info to node from egress list

* add migration logic internet gws

* create default acl policies

* add egress info

* add egress TODO

* add egress TODO

* fix user auth api:

* add reference id to acl policy

* add egress response from DB

* publish peer update on egress changes

* re initalise oauth and email config

* set verbosity

* normalise cidr on egress req

* add egress id to acl group

* change acls to use egress id

* resolve merge conflicts

* fix egress reference errors

* move egress model to schema

* add api context to DB

* sync auto update settings with hosts

* sync auto update settings with hosts

* check acl for egress node

* check for egress policy in the acl dst groups

* fix acl rules for egress policies with new models

* add status to egress model

* fix inet node func

* mask secret and convert jwt duration to minutes

* enable egress policies on creation

* convert jwt duration to minutes

* add relevant ranges to inet egress

* skip non active egress routes

* resolve merge conflicts

* fix static check

* notify peers after settings update

* define schema for activity, add api handler to list network activity

* setup event channel and logger

* setup event logger, add event for user login

* change activity model to event

* add api error constants

* add logout event

* log user crud events

* add login events for oauth

* add user related events

* log events for invites and user approvals

* order user activity event by timestamp

* fix logout api

* add user and network events api, add addtional events triggers

* add filters to all events api

* fix events filter

* add diff to event logs

* update user logout api

* log settigns updates

* log events for network and host updates

* check for diff on events

* log host del event

* add user loc info to desktop app connection events

* fix authorize middleware check

* add gateway events

* resolve merge conflicts

---------

Co-authored-by: Vishal Dalwadi <dalwadivishal26@gmail.com>
This commit is contained in:
Abhishek K 2025-05-21 13:13:20 +05:30 committed by GitHub
parent 307a3d1e4b
commit d7bad9865a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1203 additions and 31 deletions

View file

@ -268,6 +268,22 @@ func createAcl(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: acl.ID,
Name: acl.Name,
Type: models.AclSub,
},
NetworkID: acl.NetworkID,
Origin: models.Dashboard,
})
go mq.PublishPeerUpdate(true)
logic.ReturnSuccessResponseWithJson(w, r, acl, "created acl successfully")
}
@ -310,6 +326,26 @@ func updateAcl(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: acl.ID,
Name: acl.Name,
Type: models.AclSub,
},
Diff: models.Diff{
Old: acl,
New: updateAcl.Acl,
},
NetworkID: acl.NetworkID,
Origin: models.Dashboard,
})
go mq.PublishPeerUpdate(true)
logic.ReturnSuccessResponse(w, r, "updated acl "+acl.Name)
}
@ -341,6 +377,22 @@ func deleteAcl(w http.ResponseWriter, r *http.Request) {
logic.FormatError(errors.New("cannot delete default policy"), "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: acl.ID,
Name: acl.Name,
Type: models.AclSub,
},
NetworkID: acl.NetworkID,
Origin: models.Dashboard,
})
go mq.PublishPeerUpdate(true)
logic.ReturnSuccessResponse(w, r, "deleted acl "+acl.Name)
}

View file

@ -85,6 +85,22 @@ func createEgress(w http.ResponseWriter, r *http.Request) {
)
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: e.ID,
Name: e.Name,
Type: models.EgressSub,
},
NetworkID: models.NetworkID(e.Network),
Origin: models.Dashboard,
})
// for nodeID := range e.Nodes {
// node, err := logic.GetNodeByID(nodeID)
// if err != nil {
@ -174,6 +190,25 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
if req.Status != e.Status {
updateStatus = true
}
event := &models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: e.ID,
Name: e.Name,
Type: models.EgressSub,
},
Diff: models.Diff{
Old: e,
},
NetworkID: models.NetworkID(e.Network),
Origin: models.Dashboard,
}
e.Nodes = make(datatypes.JSONMap)
e.Tags = make(datatypes.JSONMap)
for nodeID, metric := range req.Nodes {
@ -211,6 +246,8 @@ func updateEgress(w http.ResponseWriter, r *http.Request) {
e.Status = req.Status
e.UpdateEgressStatus(db.WithContext(context.TODO()))
}
event.Diff.New = e
logic.LogEvent(event)
go mq.PublishPeerUpdate(false)
logic.ReturnSuccessResponseWithJson(w, r, e, "updated egress resource")
}
@ -237,6 +274,22 @@ func deleteEgress(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: e.ID,
Name: e.Name,
Type: models.EgressSub,
},
NetworkID: models.NetworkID(e.Network),
Origin: models.Dashboard,
})
// delete related acl policies
acls := logic.ListAcls()
for _, acl := range acls {

View file

@ -72,12 +72,32 @@ func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
keyID := params["keyID"]
err := logic.DeleteEnrollmentKey(keyID, false)
key, err := logic.GetEnrollmentKey(keyID)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
err = logic.DeleteEnrollmentKey(keyID, false)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to remove enrollment key: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: keyID,
Name: key.Tags[0],
Type: models.EnrollmentKeySub,
},
Origin: models.Dashboard,
})
logger.Log(2, r.Header.Get("user"), "deleted enrollment key", keyID)
w.WriteHeader(http.StatusOK)
}
@ -173,6 +193,21 @@ func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: newEnrollmentKey.Value,
Name: newEnrollmentKey.Tags[0],
Type: models.EnrollmentKeySub,
},
Origin: models.Dashboard,
})
logger.Log(2, r.Header.Get("user"), "created enrollment key")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(newEnrollmentKey)
@ -208,6 +243,7 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
return
}
}
currKey, _ := logic.GetEnrollmentKey(keyId)
newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
if err != nil {
@ -221,7 +257,25 @@ func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: newEnrollmentKey.Value,
Name: newEnrollmentKey.Tags[0],
Type: models.EnrollmentKeySub,
},
Diff: models.Diff{
Old: currKey,
New: newEnrollmentKey,
},
Origin: models.Dashboard,
})
slog.Info("updated enrollment key", "id", keyId)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(newEnrollmentKey)
@ -355,6 +409,25 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
ServerConf: server,
RequestedHost: newHost,
}
for _, netID := range enrollmentKey.Networks {
logic.LogEvent(&models.Event{
Action: models.JoinHostToNet,
Source: models.Subject{
ID: enrollmentKey.Value,
Name: enrollmentKey.Tags[0],
Type: models.EnrollmentKeySub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: newHost.ID.String(),
Name: newHost.Name,
Type: models.DeviceSub,
},
NetworkID: models.NetworkID(netID),
Origin: models.Dashboard,
})
}
logger.Log(0, newHost.Name, newHost.ID.String(), "registered with Netmaker")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(&response)

View file

@ -799,6 +799,27 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
"clientid",
extclient.ClientID,
)
if extclient.RemoteAccessClientID != "" {
// if created by user from client app, log event
logic.LogEvent(&models.Event{
Action: models.Connect,
Source: models.Subject{
ID: userName,
Name: userName,
Type: models.UserSub,
},
TriggeredBy: userName,
Target: models.Subject{
ID: extclient.Network,
Name: extclient.Network,
Type: models.NetworkSub,
Info: extclient,
},
NetworkID: models.NetworkID(extclient.Network),
Origin: models.ClientApp,
})
}
w.WriteHeader(http.StatusOK)
go func() {
if err := logic.SetClientDefaultACLs(&extclient); err != nil {

View file

@ -39,6 +39,11 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
var req models.CreateGwReq
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
@ -89,7 +94,21 @@ func createGateway(w http.ResponseWriter, r *http.Request) {
)
logic.GetNodeStatus(&relayNode, false)
apiNode := relayNode.ConvertToAPINode()
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: node.ID.String(),
Name: host.Name,
Type: models.GatewaySub,
},
Origin: models.Dashboard,
})
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(apiNode)
go func() {
@ -138,6 +157,11 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
host, err := logic.GetHost(node.HostID.String())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
node.IsGw = false
logic.UpsertNode(&node)
logger.Log(1, r.Header.Get("user"), "deleted gw", nodeid, "on network", netid)
@ -200,7 +224,21 @@ func deleteGateway(w http.ResponseWriter, r *http.Request) {
}
}()
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: node.ID.String(),
Name: host.Name,
Type: models.GatewaySub,
},
Origin: models.Dashboard,
})
logic.GetNodeStatus(&node, false)
apiNode := node.ConvertToAPINode()
logger.Log(1, r.Header.Get("user"), "deleted ingress gateway", nodeid)

View file

@ -294,7 +294,25 @@ func updateHost(w http.ResponseWriter, r *http.Request) {
}
}
}()
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: currHost.ID.String(),
Name: newHost.Name,
Type: models.DeviceSub,
},
Diff: models.Diff{
Old: currHost,
New: newHost,
},
Origin: models.Dashboard,
})
apiHostData := newHost.ConvertNMHostToAPI()
logger.Log(2, r.Header.Get("user"), "updated host", newHost.ID.String())
w.WriteHeader(http.StatusOK)
@ -420,7 +438,21 @@ func deleteHost(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: currHost.ID.String(),
Name: currHost.Name,
Type: models.DeviceSub,
},
Origin: models.Dashboard,
})
apiHostData := currHost.ConvertNMHostToAPI()
logger.Log(2, r.Header.Get("user"), "removed host", currHost.Name)
w.WriteHeader(http.StatusOK)
@ -492,6 +524,22 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
r.Header.Get("user"),
fmt.Sprintf("added host %s to network %s", currHost.Name, network),
)
logic.LogEvent(&models.Event{
Action: models.JoinHostToNet,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: currHost.ID.String(),
Name: currHost.Name,
Type: models.DeviceSub,
},
NetworkID: models.NetworkID(network),
Origin: models.Dashboard,
})
w.WriteHeader(http.StatusOK)
}
@ -623,6 +671,22 @@ func deleteHostFromNetwork(w http.ResponseWriter, r *http.Request) {
logic.SetDNS()
}
}()
logic.LogEvent(&models.Event{
Action: models.RemoveHostFromNet,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: currHost.ID.String(),
Name: currHost.Name,
Type: models.DeviceSub,
},
NetworkID: models.NetworkID(network),
Origin: models.Dashboard,
})
logger.Log(
2,
r.Header.Get("user"),
@ -937,7 +1001,21 @@ func syncHost(w http.ResponseWriter, r *http.Request) {
slog.Error("failed to send host pull request", "host", host.ID.String(), "error", err)
}
}()
logic.LogEvent(&models.Event{
Action: models.Sync,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: host.ID.String(),
Name: host.Name,
Type: models.DeviceSub,
},
Origin: models.Dashboard,
})
slog.Info("requested host pull", "user", r.Header.Get("user"), "host", host.ID.String())
w.WriteHeader(http.StatusOK)
}

View file

@ -483,9 +483,9 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
}
err = logic.DeleteNetwork(network, force, doneCh)
if err != nil {
errtype := "badrequest"
errtype := logic.BadReq
if strings.Contains(err.Error(), "Node check failed") {
errtype = "forbidden"
errtype = logic.Forbidden
}
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to delete network [%s]: %v", network, err))
@ -514,6 +514,21 @@ func deleteNetwork(w http.ResponseWriter, r *http.Request) {
logic.SetDNS()
}
}()
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: network,
Name: network,
Type: models.NetworkSub,
},
Origin: models.Dashboard,
})
logger.Log(1, r.Header.Get("user"), "deleted network", network)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode("success")
@ -636,7 +651,22 @@ func createNetwork(w http.ResponseWriter, r *http.Request) {
logger.Log(1, "failed to publish peer update for default hosts after network is added")
}
}()
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: network.NetID,
Name: network.NetID,
Type: models.NetworkSub,
Info: network,
},
Origin: models.Dashboard,
})
logger.Log(1, r.Header.Get("user"), "created network", network.NetID)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(network)

View file

@ -178,7 +178,7 @@ func Authorize(
// check if host instead of user
if hostAllowed {
// TODO --- should ensure that node is only operating on itself
if hostID, _, _, err := logic.VerifyHostToken(authToken); err == nil {
if hostID, macAddr, _, err := logic.VerifyHostToken(authToken); err == nil && macAddr != "" {
r.Header.Set(hostIDHeader, hostID)
// this indicates request is from a node
// used for failover - if a getNode comes from node, this will trigger a metrics wipe
@ -650,7 +650,7 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
return
}
}
_, err = logic.GetHost(newNode.HostID.String())
host, err := logic.GetHost(newNode.HostID.String())
if err != nil {
logger.Log(0, r.Header.Get("user"),
fmt.Sprintf("failed to get host for node [ %s ] info: %v", nodeid, err))
@ -682,6 +682,25 @@ func updateNode(w http.ResponseWriter, r *http.Request) {
"on network",
currentNode.Network,
)
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: newNode.ID.String(),
Name: host.Name,
Type: models.NodeSub,
},
Diff: models.Diff{
Old: currentNode,
New: newNode,
},
Origin: models.Dashboard,
})
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(apiNode)
go func(aclUpdate, relayupdate bool, newNode *models.Node) {

View file

@ -271,6 +271,25 @@ func updateSettings(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to udpate server settings "+err.Error()), "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: models.SettingSub.String(),
Name: models.SettingSub.String(),
Type: models.SettingSub,
},
Diff: models.Diff{
Old: currSettings,
New: req,
},
Origin: models.Dashboard,
})
go reInit(currSettings, req, force == "true")
logic.ReturnSuccessResponseWithJson(w, r, req, "updated server settings successfully")
}

View file

@ -131,6 +131,22 @@ func createTag(w http.ResponseWriter, r *http.Request) {
logic.UpsertNode(&node)
}
}()
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: tag.ID.String(),
Name: tag.TagName,
Type: models.TagSub,
},
NetworkID: tag.Network,
Origin: models.Dashboard,
})
go mq.PublishPeerUpdate(false)
var res models.TagListRespNodes = models.TagListRespNodes{
@ -163,6 +179,25 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
e := &models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: tag.ID.String(),
Name: tag.TagName,
Type: models.TagSub,
},
Diff: models.Diff{
Old: tag,
},
NetworkID: tag.Network,
Origin: models.Dashboard,
}
updateTag.NewName = strings.TrimSpace(updateTag.NewName)
var newID models.TagID
if updateTag.NewName != "" {
@ -198,7 +233,8 @@ func updateTag(w http.ResponseWriter, r *http.Request) {
}
mq.PublishPeerUpdate(false)
}()
e.Diff.New = updateTag
logic.LogEvent(e)
var res models.TagListRespNodes = models.TagListRespNodes{
Tag: tag,
UsedByCnt: len(updateTag.TaggedNodes),
@ -241,5 +277,21 @@ func deleteTag(w http.ResponseWriter, r *http.Request) {
logic.RemoveTagFromEnrollmentKeys(tag.ID)
mq.PublishPeerUpdate(false)
}()
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: tag.ID.String(),
Name: tag.TagName,
Type: models.TagSub,
},
NetworkID: tag.Network,
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "deleted tag "+tagID)
}

View file

@ -43,6 +43,7 @@ func userHandlers(r *mux.Router) {
r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(createUserAccessToken))).Methods(http.MethodPost)
r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(getUserAccessTokens))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/users/access_token", logic.SecurityCheck(true, http.HandlerFunc(deleteUserAccessTokens))).Methods(http.MethodDelete)
r.HandleFunc("/api/v1/users/logout", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(logout)))).Methods(http.MethodPost)
}
// @Summary Authenticate a user to retrieve an authorization token
@ -64,25 +65,25 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
if err != nil {
logger.Log(0, "error decoding request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
return
}
if req.Name == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), "badrequest"))
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("name is required"), logic.BadReq))
return
}
if req.UserName == "" {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), "badrequest"))
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("username is required"), logic.BadReq))
return
}
caller, err := logic.GetUser(r.Header.Get("user"))
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
return
}
user, err := logic.GetUser(req.UserName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.UnAuthorized))
return
}
if caller.UserName != user.UserName && caller.PlatformRoleID != models.SuperAdminRole {
@ -106,7 +107,7 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
)
return
}
@ -115,10 +116,26 @@ func createUserAccessToken(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(errors.New("error creating access token "+err.Error()), "internal"),
logic.FormatError(errors.New("error creating access token "+err.Error()), logic.Internal),
)
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: caller.UserName,
Name: caller.UserName,
Type: models.UserSub,
},
TriggeredBy: caller.UserName,
Target: models.Subject{
ID: req.ID,
Name: req.Name,
Type: models.UserAccessTokenSub,
Info: req,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponseWithJson(w, r, models.SuccessfulUserLoginResponse{
AuthToken: jwt,
UserName: req.UserName,
@ -197,6 +214,22 @@ func deleteUserAccessTokens(w http.ResponseWriter, r *http.Request) {
)
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: caller.UserName,
Name: caller.UserName,
Type: models.UserSub,
},
TriggeredBy: caller.UserName,
Target: models.Subject{
ID: a.ID,
Name: a.Name,
Type: models.UserAccessTokenSub,
Info: a,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponseWithJson(w, r, nil, "revoked access token")
}
@ -258,6 +291,38 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) {
logic.ReturnErrorResponse(response, request, logic.FormatError(errors.New("access denied to dashboard"), "unauthorized"))
return
}
// log user activity
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.DashboardSub.String(),
Name: models.DashboardSub.String(),
Type: models.DashboardSub,
},
Origin: models.Dashboard,
})
} else {
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.ClientAppSub.String(),
Name: models.ClientAppSub.String(),
Type: models.ClientAppSub,
},
Origin: models.ClientApp,
})
}
username := authRequest.UserName
@ -614,6 +679,21 @@ func createUser(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: caller.UserName,
Name: caller.UserName,
Type: models.UserSub,
},
TriggeredBy: caller.UserName,
Target: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
Origin: models.Dashboard,
})
logic.DeleteUserInvite(user.UserName)
logic.DeletePendingUser(user.UserName)
go mq.PublishPeerUpdate(false)
@ -752,6 +832,25 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
if userchange.PlatformRoleID != user.PlatformRoleID || !logic.CompareMaps(user.UserGroups, userchange.UserGroups) {
(&schema.UserAccessToken{UserName: user.UserName}).DeleteAllUserTokens(r.Context())
}
e := models.Event{
Action: models.Update,
Source: models.Subject{
ID: caller.UserName,
Name: caller.UserName,
Type: models.UserSub,
},
TriggeredBy: caller.UserName,
Target: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
Diff: models.Diff{
Old: user,
New: userchange,
},
Origin: models.Dashboard,
}
user, err = logic.UpdateUser(&userchange, user)
if err != nil {
logger.Log(0, username,
@ -759,6 +858,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
logic.LogEvent(&e)
go mq.PublishPeerUpdate(false)
logger.Log(1, username, "was updated")
json.NewEncoder(w).Encode(logic.ToReturnUser(*user))
@ -837,6 +937,21 @@ func deleteUser(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: caller.UserName,
Name: caller.UserName,
Type: models.UserSub,
},
TriggeredBy: caller.UserName,
Target: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
Origin: models.Dashboard,
})
// check and delete extclient with this ownerID
go func() {
extclients, err := logic.GetAllExtClients()
@ -902,3 +1017,50 @@ func listRoles(w http.ResponseWriter, r *http.Request) {
logic.ReturnSuccessResponseWithJson(w, r, roles, "successfully fetched user roles permission templates")
}
// swagger:route POST /api/v1/user/logout user logout
//
// LogOut user.
//
// Schemes: https
//
// Security:
// oauth
//
// Responses:
// 200: userBodyResponse
func logout(w http.ResponseWriter, r *http.Request) {
// set header.
w.Header().Set("Content-Type", "application/json")
userName := r.URL.Query().Get("username")
user, err := logic.GetUser(userName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
return
}
var target models.SubjectType
if val := r.Header.Get("From-Ui"); val == "true" {
target = models.DashboardSub
} else {
target = models.ClientAppSub
}
if target != "" {
logic.LogEvent(&models.Event{
Action: models.LogOut,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: target.String(),
Name: target.String(),
Type: target,
},
Origin: models.Origin(target),
})
}
logic.ReturnSuccessResponse(w, r, "user logged out")
}

View file

@ -83,6 +83,18 @@ func FromContext(ctx context.Context) *gorm.DB {
return db
}
func SetPagination(ctx context.Context, page, pageSize int) context.Context {
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
db := FromContext(ctx)
offset := (page - 1) * pageSize
return context.WithValue(ctx, dbCtxKey, db.Offset(offset).Limit(pageSize))
}
// BeginTx returns a context with a new transaction.
// If the context already has a db connection instance,
// it uses that instance. Otherwise, it uses the

1
go.mod
View file

@ -59,6 +59,7 @@ require (
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect

2
go.sum
View file

@ -40,6 +40,8 @@ github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=

View file

@ -8,20 +8,30 @@ import (
"golang.org/x/exp/slog"
)
type ApiErrorType string
const (
Internal ApiErrorType = "internal"
BadReq ApiErrorType = "badrequest"
NotFound ApiErrorType = "notfound"
UnAuthorized ApiErrorType = "unauthorized"
Forbidden ApiErrorType = "forbidden"
)
// FormatError - takes ErrorResponse and uses correct code
func FormatError(err error, errType string) models.ErrorResponse {
func FormatError(err error, errType ApiErrorType) models.ErrorResponse {
var status = http.StatusInternalServerError
switch errType {
case "internal":
case Internal:
status = http.StatusInternalServerError
case "badrequest":
case BadReq:
status = http.StatusBadRequest
case "notfound":
case NotFound:
status = http.StatusNotFound
case "unauthorized":
case UnAuthorized:
status = http.StatusUnauthorized
case "forbidden":
case Forbidden:
status = http.StatusForbidden
default:
status = http.StatusInternalServerError

View file

@ -125,6 +125,25 @@ func DeleteExtClient(network string, clientid string) error {
}
deleteExtClientFromCache(key)
}
if extClient.RemoteAccessClientID != "" {
LogEvent(&models.Event{
Action: models.Disconnect,
Source: models.Subject{
ID: extClient.OwnerID,
Name: extClient.OwnerID,
Type: models.UserSub,
},
TriggeredBy: extClient.OwnerID,
Target: models.Subject{
ID: extClient.Network,
Name: extClient.Network,
Type: models.NetworkSub,
Info: extClient,
},
NetworkID: models.NetworkID(extClient.Network),
Origin: models.ClientApp,
})
}
go RemoveNodeFromAclPolicy(extClient.ConvertToStaticNode())
return nil
}

View file

@ -20,6 +20,8 @@ var (
telServerRecord = models.Telemetry{}
)
var LogEvent = func(a *models.Event) {}
// posthog_pub_key - Key for sending data to PostHog
const posthog_pub_key = "phc_1vEXhPOA1P7HP5jP2dVU9xDTUqXHAelmtravyZ1vvES"

74
models/events.go Normal file
View file

@ -0,0 +1,74 @@
package models
type Action string
const (
Create Action = "CREATE"
Update Action = "UPDATE"
Delete Action = "DELETE"
DeleteAll Action = "DELETE_ALL"
Login Action = "LOGIN"
LogOut Action = "LOGOUT"
Connect Action = "CONNECT"
Sync Action = "SYNC"
Disconnect Action = "DISCONNECT"
JoinHostToNet Action = "JOIN_HOST_TO_NETWORK"
RemoveHostFromNet Action = "REMOVE_HOST_FROM_NETWORK"
)
type SubjectType string
const (
UserSub SubjectType = "USER"
UserAccessTokenSub SubjectType = "USER_ACCESS_TOKEN"
DeviceSub SubjectType = "DEVICE"
NodeSub SubjectType = "NODE"
GatewaySub SubjectType = "GATEWAY"
SettingSub SubjectType = "SETTING"
AclSub SubjectType = "ACL"
TagSub SubjectType = "TAG"
UserRoleSub SubjectType = "USER_ROLE"
UserGroupSub SubjectType = "USER_GROUP"
UserInviteSub SubjectType = "USER_INVITE"
PendingUserSub SubjectType = "PENDING_USER"
EgressSub SubjectType = "EGRESS"
NetworkSub SubjectType = "NETWORK"
DashboardSub SubjectType = "DASHBOARD"
EnrollmentKeySub SubjectType = "ENROLLMENT_KEY"
ClientAppSub SubjectType = "CLIENT-APP"
)
func (sub SubjectType) String() string {
return string(sub)
}
type Origin string
const (
Dashboard Origin = "DASHBOARD"
Api Origin = "API"
NMCTL Origin = "NMCTL"
ClientApp Origin = "CLIENT-APP"
)
type Subject struct {
ID string `json:"id"`
Name string `json:"name"`
Type SubjectType `json:"subject_type"`
Info interface{} `json:"info"`
}
type Diff struct {
Old interface{}
New interface{}
}
type Event struct {
Action Action
Source Subject
Origin Origin
Target Subject
TriggeredBy string
NetworkID NetworkID
Diff Diff
}

View file

@ -176,7 +176,22 @@ func handleAzureCallback(w http.ResponseWriter, r *http.Request) {
logger.Log(1, "could not parse jwt for user", authRequest.UserName)
return
}
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.DashboardSub.String(),
Name: models.DashboardSub.String(),
Type: models.DashboardSub,
Info: user,
},
Origin: models.Dashboard,
})
logger.Log(1, "completed azure OAuth sigin in for", content.Email)
http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
}

View file

@ -167,7 +167,22 @@ func handleGithubCallback(w http.ResponseWriter, r *http.Request) {
logger.Log(1, "could not parse jwt for user", authRequest.UserName)
return
}
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.DashboardSub.String(),
Name: models.DashboardSub.String(),
Type: models.DashboardSub,
Info: user,
},
Origin: models.Dashboard,
})
logger.Log(1, "completed github OAuth sigin in for", content.Email)
http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
}

View file

@ -69,6 +69,7 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
handleOauthNotConfigured(w)
return
}
var inviteExists bool
// check if invite exists for User
in, err := logic.GetUserInvite(content.Email)
@ -160,6 +161,23 @@ func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
return
}
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.DashboardSub.String(),
Name: models.DashboardSub.String(),
Type: models.DashboardSub,
Info: user,
},
Origin: models.Dashboard,
})
logger.Log(1, "completed google OAuth sigin in for", content.Email)
http.Redirect(w, r, fmt.Sprintf("%s/login?login=%s&user=%s", servercfg.GetFrontendURL(), jwt, content.Email), http.StatusPermanentRedirect)
}

View file

@ -167,7 +167,22 @@ func handleOIDCCallback(w http.ResponseWriter, r *http.Request) {
logger.Log(1, "could not parse jwt for user", authRequest.UserName, jwtErr.Error())
return
}
logic.LogEvent(&models.Event{
Action: models.Login,
Source: models.Subject{
ID: user.UserName,
Name: user.UserName,
Type: models.UserSub,
},
TriggeredBy: user.UserName,
Target: models.Subject{
ID: models.DashboardSub.String(),
Name: models.DashboardSub.String(),
Type: models.DashboardSub,
Info: user,
},
Origin: models.Dashboard,
})
logger.Log(1, "completed OIDC OAuth signin in for", content.Email)
http.Redirect(w, r, servercfg.GetFrontendURL()+"/login?login="+jwt+"&user="+content.Email, http.StatusPermanentRedirect)
}

114
pro/controllers/events.go Normal file
View file

@ -0,0 +1,114 @@
package controllers
import (
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
)
func EventHandlers(r *mux.Router) {
r.HandleFunc("/api/v1/network/activity", logic.SecurityCheck(true, http.HandlerFunc(listNetworkActivity))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/user/activity", logic.SecurityCheck(true, http.HandlerFunc(listUserActivity))).Methods(http.MethodGet)
r.HandleFunc("/api/v1/activity", logic.SecurityCheck(true, http.HandlerFunc(listActivity))).Methods(http.MethodGet)
}
// @Summary list activity.
// @Router /api/v1/activity [get]
// @Tags Activity
// @Param network_id query string true "network_id required to get the network events"
// @Success 200 {object} models.ReturnSuccessResponseWithJson
// @Failure 500 {object} models.ErrorResponse
func listNetworkActivity(w http.ResponseWriter, r *http.Request) {
netID := r.URL.Query().Get("network_id")
// Parse query parameters with defaults
if netID == "" {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "network_id param is missing",
})
return
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
ctx := db.WithContext(r.Context())
netActivity, err := (&schema.Event{NetworkID: models.NetworkID(netID)}).ListByNetwork(db.SetPagination(ctx, page, pageSize))
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, netActivity, "successfully fetched network activity")
}
// @Summary list activity.
// @Router /api/v1/activity [get]
// @Tags Activity
// @Param network_id query string true "network_id required to get the network events"
// @Success 200 {object} models.ReturnSuccessResponseWithJson
// @Failure 500 {object} models.ErrorResponse
func listUserActivity(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
// Parse query parameters with defaults
if username == "" {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: "username param is missing",
})
return
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
ctx := db.WithContext(r.Context())
userActivity, err := (&schema.Event{TriggeredBy: username}).ListByUser(db.SetPagination(ctx, page, pageSize))
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, userActivity, "successfully fetched user activity "+username)
}
// @Summary list activity.
// @Router /api/v1/activity [get]
// @Tags Activity
// @Success 200 {object} models.ReturnSuccessResponseWithJson
// @Failure 500 {object} models.ErrorResponse
func listActivity(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
network := r.URL.Query().Get("network_id")
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
ctx := db.WithContext(r.Context())
var err error
var events []schema.Event
e := &schema.Event{TriggeredBy: username, NetworkID: models.NetworkID(network)}
if username != "" && network != "" {
events, err = e.ListByUserAndNetwork(db.SetPagination(ctx, page, pageSize))
} else if username != "" && network == "" {
events, err = e.ListByUser(db.SetPagination(ctx, page, pageSize))
} else if username == "" && network != "" {
events, err = e.ListByNetwork(db.SetPagination(ctx, page, pageSize))
} else {
events, err = e.List(db.SetPagination(ctx, page, pageSize))
}
if err != nil {
logic.ReturnErrorResponse(w, r, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
logic.ReturnSuccessResponseWithJson(w, r, events, "successfully fetched all events ")
}

View file

@ -62,7 +62,6 @@ func UserHandlers(r *mux.Router) {
r.HandleFunc("/api/users/{username}/remote_access_gw/{remote_access_gateway_id}", logic.SecurityCheck(true, http.HandlerFunc(removeUserFromRemoteAccessGW))).Methods(http.MethodDelete)
r.HandleFunc("/api/users/{username}/remote_access_gw", logic.SecurityCheck(false, logic.ContinueIfUserMatch(http.HandlerFunc(getUserRemoteAccessGwsV1)))).Methods(http.MethodGet)
r.HandleFunc("/api/users/ingress/{ingress_id}", logic.SecurityCheck(true, http.HandlerFunc(ingressGatewayUsers))).Methods(http.MethodGet)
}
// swagger:route POST /api/v1/users/invite-signup user userInviteSignUp
@ -248,6 +247,21 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
if err != nil {
slog.Error("failed to insert invite for user", "email", invite.Email, "error", err)
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: callerUserName,
Name: callerUserName,
Type: models.UserSub,
},
TriggeredBy: callerUserName,
Target: models.Subject{
ID: inviteeEmail,
Name: inviteeEmail,
Type: models.UserInviteSub,
},
Origin: models.Dashboard,
})
// notify user with magic link
go func(invite models.UserInvite) {
// Set E-Mail body. You can set plain text or html with text/html
@ -266,6 +280,7 @@ func inviteUsers(w http.ResponseWriter, r *http.Request) {
}
}(invite)
}
logic.ReturnSuccessResponse(w, r, "triggered user invites")
}
@ -309,6 +324,21 @@ func deleteUserInvite(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: email,
Name: email,
Type: models.UserInviteSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "deleted user invite")
}
@ -463,6 +493,21 @@ func createUserGroup(w http.ResponseWriter, r *http.Request) {
user.UserGroups[userGroupReq.Group.ID] = struct{}{}
logic.UpsertUser(*user)
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userGroupReq.Group.ID.String(),
Name: userGroupReq.Group.Name,
Type: models.UserGroupSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponseWithJson(w, r, userGroupReq.Group, "created user group")
}
@ -506,7 +551,25 @@ func updateUserGroup(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userGroup.ID.String(),
Name: userGroup.Name,
Type: models.UserGroupSub,
},
Diff: models.Diff{
Old: currUserG,
New: userGroup,
},
Origin: models.Dashboard,
})
// reset configs for service user
go proLogic.UpdatesUserGwAccessOnGrpUpdates(currUserG.NetworkRoles, userGroup.NetworkRoles)
logic.ReturnSuccessResponseWithJson(w, r, userGroup, "updated user group")
@ -551,6 +614,21 @@ func deleteUserGroup(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userG.ID.String(),
Name: userG.Name,
Type: models.UserGroupSub,
},
Origin: models.Dashboard,
})
go proLogic.UpdatesUserGwAccessOnGrpUpdates(userG.NetworkRoles, make(map[models.NetworkID]map[models.UserRoleID]struct{}))
logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user group")
}
@ -631,6 +709,21 @@ func createRole(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userRole.ID.String(),
Name: userRole.Name,
Type: models.UserRoleSub,
},
Origin: models.ClientApp,
})
logic.ReturnSuccessResponseWithJson(w, r, userRole, "created user role")
}
@ -665,6 +758,25 @@ func updateRole(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Update,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: userRole.ID.String(),
Name: userRole.Name,
Type: models.UserRoleSub,
},
Diff: models.Diff{
Old: currRole,
New: userRole,
},
Origin: models.Dashboard,
})
// reset configs for service user
go proLogic.UpdatesUserGwAccessOnRoleUpdates(currRole.NetworkLevelAccess, userRole.NetworkLevelAccess, string(userRole.NetworkID))
logic.ReturnSuccessResponseWithJson(w, r, userRole, "updated user role")
@ -693,6 +805,21 @@ func deleteRole(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: role.ID.String(),
Name: role.Name,
Type: models.UserRoleSub,
},
Origin: models.Dashboard,
})
go proLogic.UpdatesUserGwAccessOnRoleUpdates(role.NetworkLevelAccess, make(map[models.RsrcType]map[models.RsrcID]models.RsrcPermissionScope), role.NetworkID.String())
logic.ReturnSuccessResponseWithJson(w, r, nil, "deleted user role")
}
@ -1349,6 +1476,21 @@ func approvePendingUser(w http.ResponseWriter, r *http.Request) {
break
}
}
logic.LogEvent(&models.Event{
Action: models.Create,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: username,
Name: username,
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "approved "+username)
}
@ -1380,6 +1522,21 @@ func deletePendingUser(w http.ResponseWriter, r *http.Request) {
break
}
}
logic.LogEvent(&models.Event{
Action: models.Delete,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: username,
Name: username,
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "deleted pending "+username)
}
@ -1395,5 +1552,20 @@ func deleteAllPendingUsers(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("failed to delete all pending users "+err.Error()), "internal"))
return
}
logic.LogEvent(&models.Event{
Action: models.DeleteAll,
Source: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.UserSub,
},
TriggeredBy: r.Header.Get("user"),
Target: models.Subject{
ID: r.Header.Get("user"),
Name: r.Header.Get("user"),
Type: models.PendingUserSub,
},
Origin: models.Dashboard,
})
logic.ReturnSuccessResponse(w, r, "cleared all pending users")
}

View file

@ -34,6 +34,7 @@ func InitPro() {
proControllers.FailOverHandlers,
proControllers.InetHandlers,
proControllers.RacHandlers,
proControllers.EventHandlers,
)
controller.ListRoles = proControllers.ListRoles
logic.EnterpriseCheckFuncs = append(logic.EnterpriseCheckFuncs, func() {
@ -93,6 +94,7 @@ func InitPro() {
proLogic.LoadNodeMetricsToCache()
proLogic.InitFailOverCache()
email.Init()
proLogic.EventWatcher()
})
logic.ResetFailOver = proLogic.ResetFailOver
logic.ResetFailedOverPeer = proLogic.ResetFailedOverPeer
@ -140,6 +142,7 @@ func InitPro() {
logic.GetNodeStatus = proLogic.GetNodeStatus
logic.InitializeAuthProvider = auth.InitializeAuthProvider
logic.EmailInit = email.Init
logic.LogEvent = proLogic.LogEvent
}
func retrieveProLogo() string {

47
pro/logic/events.go Normal file
View file

@ -0,0 +1,47 @@
package logic
import (
"context"
"encoding/json"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/schema"
)
var EventActivityCh = make(chan models.Event, 100)
func LogEvent(a *models.Event) {
EventActivityCh <- *a
}
func EventWatcher() {
for e := range EventActivityCh {
if e.Action == models.Update {
// check if diff
if cmp.Equal(e.Diff.Old, e.Diff.New) {
continue
}
}
sourceJson, _ := json.Marshal(e.Source)
dstJson, _ := json.Marshal(e.Target)
diff, _ := json.Marshal(e.Diff)
a := schema.Event{
ID: uuid.New().String(),
Action: e.Action,
Source: sourceJson,
Target: dstJson,
Origin: e.Origin,
NetworkID: e.NetworkID,
TriggeredBy: e.TriggeredBy,
Diff: diff,
TimeStamp: time.Now().UTC(),
}
a.Create(db.WithContext(context.TODO()))
}
}

55
schema/event.go Normal file
View file

@ -0,0 +1,55 @@
package schema
import (
"context"
"time"
"github.com/gravitl/netmaker/db"
"github.com/gravitl/netmaker/models"
"gorm.io/datatypes"
)
type Event struct {
ID string `gorm:"primaryKey" json:"id"`
Action models.Action `gorm:"action" json:"action"`
Source datatypes.JSON `gorm:"source" json:"source"`
Origin models.Origin `gorm:"origin" json:"origin"`
Target datatypes.JSON `gorm:"target" json:"target"`
NetworkID models.NetworkID `gorm:"network_id" json:"network_id"`
TriggeredBy string `gorm:"triggered_by" json:"triggered_by"`
Diff datatypes.JSON `gorm:"diff" json:"diff"`
TimeStamp time.Time `gorm:"time_stamp" json:"time_stamp"`
}
func (a *Event) Get(ctx context.Context) error {
return db.FromContext(ctx).Model(&Event{}).First(&a).Where("id = ?", a.ID).Error
}
func (a *Event) Update(ctx context.Context) error {
return db.FromContext(ctx).Model(&Event{}).Where("id = ?", a.ID).Updates(&a).Error
}
func (a *Event) Create(ctx context.Context) error {
return db.FromContext(ctx).Model(&Event{}).Create(&a).Error
}
func (a *Event) ListByNetwork(ctx context.Context) (ats []Event, err error) {
err = db.FromContext(ctx).Model(&Event{}).Where("network_id = ?", a.NetworkID).Order("time_stamp DESC").Find(&ats).Error
return
}
func (a *Event) ListByUser(ctx context.Context) (ats []Event, err error) {
err = db.FromContext(ctx).Model(&Event{}).Where("triggered_by = ?", a.TriggeredBy).Order("time_stamp DESC").Find(&ats).Error
return
}
func (a *Event) ListByUserAndNetwork(ctx context.Context) (ats []Event, err error) {
err = db.FromContext(ctx).Model(&Event{}).Where("network_id = ? AND triggered_by = ?",
a.NetworkID, a.TriggeredBy).Order("time_stamp DESC").Find(&ats).Error
return
}
func (a *Event) List(ctx context.Context) (ats []Event, err error) {
err = db.FromContext(ctx).Model(&Event{}).Order("time_stamp DESC").Find(&ats).Error
return
}

View file

@ -6,5 +6,6 @@ func ListModels() []interface{} {
&Job{},
&Egress{},
&UserAccessToken{},
&Event{},
}
}