netmaker/controllers/enrollmentkeys.go
Abhishek K d7bad9865a
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>
2025-05-21 13:13:20 +05:30

436 lines
14 KiB
Go

package controller
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gravitl/netmaker/auth"
"github.com/gravitl/netmaker/logger"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
"github.com/gravitl/netmaker/mq"
"github.com/gravitl/netmaker/servercfg"
"golang.org/x/exp/slog"
)
func enrollmentKeyHandlers(r *mux.Router) {
r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(createEnrollmentKey))).
Methods(http.MethodPost)
r.HandleFunc("/api/v1/enrollment-keys", logic.SecurityCheck(true, http.HandlerFunc(getEnrollmentKeys))).
Methods(http.MethodGet)
r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(deleteEnrollmentKey))).
Methods(http.MethodDelete)
r.HandleFunc("/api/v1/host/register/{token}", http.HandlerFunc(handleHostRegister)).
Methods(http.MethodPost)
r.HandleFunc("/api/v1/enrollment-keys/{keyID}", logic.SecurityCheck(true, http.HandlerFunc(updateEnrollmentKey))).
Methods(http.MethodPut)
}
// @Summary Lists all EnrollmentKeys for admins
// @Router /api/v1/enrollment-keys [get]
// @Tags EnrollmentKeys
// @Security oauth
// @Success 200 {array} models.EnrollmentKey
// @Failure 500 {object} models.ErrorResponse
func getEnrollmentKeys(w http.ResponseWriter, r *http.Request) {
keys, err := logic.GetAllEnrollmentKeys()
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to fetch enrollment keys: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
ret := []*models.EnrollmentKey{}
for _, key := range keys {
key := key
if err = logic.Tokenize(&key, servercfg.GetAPIHost()); err != nil {
logger.Log(0, r.Header.Get("user"), "failed to get token values for keys:", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
ret = append(ret, &key)
}
// return JSON/API formatted keys
logger.Log(2, r.Header.Get("user"), "fetched enrollment keys")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(ret)
}
// @Summary Deletes an EnrollmentKey from Netmaker server
// @Router /api/v1/enrollment-keys/{keyid} [delete]
// @Tags EnrollmentKeys
// @Security oauth
// @Param keyid path string true "Enrollment Key ID"
// @Success 200
// @Failure 500 {object} models.ErrorResponse
func deleteEnrollmentKey(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
keyID := params["keyID"]
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)
}
// @Summary Creates an EnrollmentKey for hosts to register with server and join networks
// @Router /api/v1/enrollment-keys [post]
// @Tags EnrollmentKeys
// @Security oauth
// @Param body body models.APIEnrollmentKey true "Enrollment Key parameters"
// @Success 200 {object} models.EnrollmentKey
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func createEnrollmentKey(w http.ResponseWriter, r *http.Request) {
var enrollmentKeyBody models.APIEnrollmentKey
err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
var newTime time.Time
if enrollmentKeyBody.Expiration > 0 {
newTime = time.Unix(enrollmentKeyBody.Expiration, 0)
}
v := validator.New()
err = v.Struct(enrollmentKeyBody)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error validating request body: ",
err.Error())
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(
fmt.Errorf("validation error: name length must be between 3 and 32: %w", err),
"badrequest",
),
)
return
}
if existingKeys, err := logic.GetAllEnrollmentKeys(); err != nil {
logger.Log(0, r.Header.Get("user"), "error validating request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
} else {
// check if any tags are duplicate
existingTags := make(map[string]struct{})
for _, existingKey := range existingKeys {
for _, t := range existingKey.Tags {
existingTags[t] = struct{}{}
}
}
for _, t := range enrollmentKeyBody.Tags {
if _, ok := existingTags[t]; ok {
logic.ReturnErrorResponse(w, r, logic.FormatError(fmt.Errorf("key names must be unique"), "badrequest"))
return
}
}
}
relayId := uuid.Nil
if enrollmentKeyBody.Relay != "" {
relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
if err != nil {
logger.Log(0, r.Header.Get("user"), "error parsing relay id: ", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
newEnrollmentKey, err := logic.CreateEnrollmentKey(
enrollmentKeyBody.UsesRemaining,
newTime,
enrollmentKeyBody.Networks,
enrollmentKeyBody.Tags,
enrollmentKeyBody.Groups,
enrollmentKeyBody.Unlimited,
relayId,
false,
enrollmentKeyBody.AutoEgress,
)
if err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
logger.Log(0, r.Header.Get("user"), "failed to create enrollment key:", err.Error())
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)
}
// @Summary Updates an EnrollmentKey. Updates are only limited to the relay to use
// @Router /api/v1/enrollment-keys/{keyid} [put]
// @Tags EnrollmentKeys
// @Security oauth
// @Param keyid path string true "Enrollment Key ID"
// @Param body body models.APIEnrollmentKey true "Enrollment Key parameters"
// @Success 200 {object} models.EnrollmentKey
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func updateEnrollmentKey(w http.ResponseWriter, r *http.Request) {
var enrollmentKeyBody models.APIEnrollmentKey
params := mux.Vars(r)
keyId := params["keyID"]
err := json.NewDecoder(r.Body).Decode(&enrollmentKeyBody)
if err != nil {
slog.Error("error decoding request body", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
relayId := uuid.Nil
if enrollmentKeyBody.Relay != "" {
relayId, err = uuid.Parse(enrollmentKeyBody.Relay)
if err != nil {
slog.Error("error parsing relay id", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
}
currKey, _ := logic.GetEnrollmentKey(keyId)
newEnrollmentKey, err := logic.UpdateEnrollmentKey(keyId, relayId, enrollmentKeyBody.Groups)
if err != nil {
slog.Error("failed to update enrollment key", "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
if err = logic.Tokenize(newEnrollmentKey, servercfg.GetAPIHost()); err != nil {
slog.Error("failed to update enrollment key", "error", err)
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)
}
// @Summary Handles a Netclient registration with server and add nodes accordingly
// @Router /api/v1/host/register/{token} [post]
// @Tags EnrollmentKeys
// @Security oauth
// @Param token path string true "Enrollment Key Token"
// @Param body body models.Host true "Host registration parameters"
// @Success 200 {object} models.RegisterResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
func handleHostRegister(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
token := params["token"]
logger.Log(0, "received registration attempt with token", token)
// check if token exists
enrollmentKey, err := logic.DeTokenize(token)
if err != nil {
logger.Log(0, "invalid enrollment key used", token, err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
// get the host
var newHost models.Host
if err = json.NewDecoder(r.Body).Decode(&newHost); err != nil {
logger.Log(0, r.Header.Get("user"), "error decoding request body: ",
err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
// check if host already exists
hostExists := false
if hostExists = logic.HostExists(&newHost); hostExists && len(enrollmentKey.Networks) == 0 {
logger.Log(
0,
"host",
newHost.ID.String(),
newHost.Name,
"attempted to re-register with no networks",
)
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("host already exists"), "badrequest"),
)
return
}
// version check
if !logic.IsVersionCompatible(newHost.Version) {
err := fmt.Errorf("bad client version on register: %s", newHost.Version)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
if newHost.TrafficKeyPublic == nil && newHost.OS != models.OS_Types.IoT {
err := fmt.Errorf("missing traffic key")
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
return
}
key, keyErr := logic.RetrievePublicTrafficKey()
if keyErr != nil {
logger.Log(0, "error retrieving key:", keyErr.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(keyErr, "internal"))
return
}
// use the token
if ok := logic.TryToUseEnrollmentKey(enrollmentKey); !ok {
logger.Log(0, "host", newHost.ID.String(), newHost.Name, "failed registration")
logic.ReturnErrorResponse(
w,
r,
logic.FormatError(fmt.Errorf("invalid enrollment key"), "badrequest"),
)
return
}
if !hostExists {
newHost.PersistentKeepalive = models.DefaultPersistentKeepAlive
// register host
_ = logic.CheckHostPorts(&newHost)
// create EMQX credentials and ACLs for host
if servercfg.GetBrokerType() == servercfg.EmqxBrokerType {
if err := mq.GetEmqxHandler().CreateEmqxUser(newHost.ID.String(), newHost.HostPass); err != nil {
logger.Log(0, "failed to create host credentials for EMQX: ", err.Error())
return
}
}
if err = logic.CreateHost(&newHost); err != nil {
logger.Log(
0,
"host",
newHost.ID.String(),
newHost.Name,
"failed registration -",
err.Error(),
)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
} else {
// need to revise the list of networks from key
// based on the ones host currently has
networksToAdd := []string{}
currentNets := logic.GetHostNetworks(newHost.ID.String())
for _, newNet := range enrollmentKey.Networks {
if !logic.StringSliceContains(currentNets, newNet) {
networksToAdd = append(networksToAdd, newNet)
}
}
enrollmentKey.Networks = networksToAdd
currHost, err := logic.GetHost(newHost.ID.String())
if err != nil {
slog.Error("failed registration", "hostID", newHost.ID.String(), "hostName", newHost.Name, "error", err.Error())
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
logic.UpdateHostFromClient(&newHost, currHost)
err = logic.UpsertHost(currHost)
if err != nil {
slog.Error("failed to update host", "id", currHost.ID, "error", err)
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
}
// ready the response
server := logic.GetServerInfo()
server.TrafficKey = key
response := models.RegisterResponse{
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)
// notify host of changes, peer and node updates
go auth.CheckNetRegAndHostUpdate(enrollmentKey.Networks, &newHost, enrollmentKey.Relay, enrollmentKey.Groups)
}