mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
Merge branch 'NM-356-apikeys' of https://github.com/gravitl/netmaker into NM-356-apikeys
This commit is contained in:
+11
-5
@@ -223,6 +223,11 @@ func applyDeviceNetworkHostState(ctx context.Context, host *schema.Host, network
|
||||
if node, err := getHostNodeOnNetwork(ctx, host, network.Name); err == nil {
|
||||
dn.Joined = true
|
||||
dn.Connected = node.Connected
|
||||
violations, _ := CheckPostureViolationsForHost(ctx, host, nil, schema.NetworkID(network.Name), true)
|
||||
if len(violations) > 0 {
|
||||
dn.Status = models.DeviceNetworkStatusBlocked
|
||||
return
|
||||
}
|
||||
if node.Connected {
|
||||
dn.Status = models.DeviceNetworkStatusJoined
|
||||
} else {
|
||||
@@ -284,6 +289,12 @@ func JoinDeviceNetwork(ctx context.Context, user *schema.User, host *schema.Host
|
||||
if err := network.Get(ctx); err != nil {
|
||||
return empty, fmt.Errorf("network not found: %w", err)
|
||||
}
|
||||
|
||||
violations, _ := CheckPostureViolationsForHost(ctx, host, nil, schema.NetworkID(networkID), true)
|
||||
if len(violations) > 0 {
|
||||
return empty, errors.New("access blocked: this device doesn't meet security requirements")
|
||||
}
|
||||
|
||||
if DoesHostExistInTheNetworkAlready(host, network) {
|
||||
// Node may already exist but be disconnected after JIT expiry — reconnect.
|
||||
if err := reconnectUserDeviceNode(ctx, host, networkID); err != nil {
|
||||
@@ -292,11 +303,6 @@ func JoinDeviceNetwork(ctx context.Context, user *schema.User, host *schema.Host
|
||||
return models.DeviceJoinResult{Status: models.DeviceJoinStatusJoined}, nil
|
||||
}
|
||||
|
||||
violations, _ := CheckPostureViolationsForHost(ctx, host, nil, schema.NetworkID(networkID), true)
|
||||
if len(violations) > 0 {
|
||||
return empty, errors.New("access blocked: this device doesn't meet security requirements")
|
||||
}
|
||||
|
||||
if deviceJoinRequiresApproval(ctx, *network, user) {
|
||||
p := &schema.PendingHost{HostID: host.ID.String(), Network: networkID}
|
||||
if err := p.CheckIfPendingHostExists(ctx); err == nil {
|
||||
|
||||
@@ -637,3 +637,79 @@ func TestRegisterDevice_rejectsCrossTenantHost(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "host already registered to another tenant", err.Error())
|
||||
}
|
||||
|
||||
func TestJoinDeviceNetworkBlocksReconnectOnPostureViolation(t *testing.T) {
|
||||
ctx := scopedTestContext(t)
|
||||
netName := "posture-reconnect-" + uuid.NewString()[:8]
|
||||
username := "posture-reconnect-user-" + uuid.NewString()[:8]
|
||||
|
||||
require.NoError(t, CreateNetwork(ctx, &schema.Network{
|
||||
TenantID: scope.ID(ctx),
|
||||
Name: netName,
|
||||
AddressRange: "10.91.0.0/24",
|
||||
AutoJoin: true,
|
||||
}))
|
||||
t.Cleanup(func() { _ = (&schema.Network{Name: netName}).Delete(ctx) })
|
||||
|
||||
net := &schema.Network{Name: netName}
|
||||
require.NoError(t, net.Get(ctx))
|
||||
|
||||
user := &schema.User{Username: username, PlatformRoleID: schema.PlatformUser}
|
||||
require.NoError(t, user.Create(ctx))
|
||||
t.Cleanup(func() { _ = user.Delete(ctx) })
|
||||
|
||||
hostID := uuid.New()
|
||||
host := &schema.Host{
|
||||
ID: hostID,
|
||||
Name: "posture-reconnect-host",
|
||||
OS: "linux",
|
||||
Version: "dev",
|
||||
HostPass: "test-pass",
|
||||
TrafficKeyPublic: []byte{1, 2, 3},
|
||||
OwnerUsername: username,
|
||||
}
|
||||
require.NoError(t, host.Create(ctx))
|
||||
t.Cleanup(func() { _ = host.Delete(ctx) })
|
||||
|
||||
nodeID := uuid.NewString()
|
||||
node := &schema.Node{
|
||||
ID: nodeID,
|
||||
TenantID: scope.ID(ctx),
|
||||
HostID: hostID.String(),
|
||||
NetworkID: net.ID,
|
||||
Address: "10.91.0.10",
|
||||
Connected: false,
|
||||
Status: schema.Disconnected,
|
||||
}
|
||||
require.NoError(t, node.Create(ctx))
|
||||
t.Cleanup(func() { _ = node.Delete(ctx) })
|
||||
|
||||
origJITAccess := CheckJITAccess
|
||||
t.Cleanup(func() { CheckJITAccess = origJITAccess })
|
||||
CheckJITAccess = func(context.Context, string, string) (bool, *schema.JITGrant, error) {
|
||||
return true, nil, nil
|
||||
}
|
||||
|
||||
origPosture := CheckPostureViolationsForHost
|
||||
t.Cleanup(func() { CheckPostureViolationsForHost = origPosture })
|
||||
CheckPostureViolationsForHost = func(context.Context, *schema.Host, map[models.TagID]struct{}, schema.NetworkID, bool) ([]models.Violation, schema.Severity) {
|
||||
return []models.Violation{{CheckID: "os"}}, schema.SeverityHigh
|
||||
}
|
||||
|
||||
joined := false
|
||||
origJoin := JoinHostToNetworks
|
||||
t.Cleanup(func() { JoinHostToNetworks = origJoin })
|
||||
JoinHostToNetworks = func(context.Context, models.EnrollmentKey, *schema.Host, string) {
|
||||
joined = true
|
||||
}
|
||||
|
||||
_, err := JoinDeviceNetwork(ctx, user, host, netName)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "access blocked: this device doesn't meet security requirements", err.Error())
|
||||
assert.False(t, joined, "must not create a new join when posture fails on reconnect")
|
||||
|
||||
// Existing node must stay disconnected.
|
||||
check := &schema.Node{ID: nodeID}
|
||||
require.NoError(t, check.Get(ctx))
|
||||
assert.False(t, check.Connected)
|
||||
}
|
||||
|
||||
+4
-1
@@ -93,7 +93,10 @@ func CreateFallbackNameserver(network *schema.Network) error {
|
||||
"2001:4860:4860::8888",
|
||||
"2001:4860:4860::8844",
|
||||
},
|
||||
Tags: map[string]interface{}{
|
||||
Tags: map[string]any{
|
||||
"*": "",
|
||||
},
|
||||
Users: map[string]any{
|
||||
"*": "",
|
||||
},
|
||||
Status: true,
|
||||
|
||||
@@ -22,6 +22,34 @@ func IsUserOwnedHost(h *schema.Host) bool {
|
||||
return h != nil && h.OwnerUsername != ""
|
||||
}
|
||||
|
||||
// ListPostureViolatedUserOwnedDevices returns host-backed user devices on the
|
||||
// network that have a non-unknown posture severity (for Non-compliant Users).
|
||||
func ListPostureViolatedUserOwnedDevices(ctx context.Context, networkName string) ([]models.Node, error) {
|
||||
network := &schema.Network{Name: networkName}
|
||||
if err := network.Get(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_nodes, err := (&schema.Node{}).ListAll(
|
||||
ctx,
|
||||
dbtypes.WithPreloads("Host"),
|
||||
dbtypes.WithFilter("network_id", network.ID),
|
||||
dbtypes.WithNotFilter("posture_check_severity", schema.SeverityUnknown),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]models.Node, 0)
|
||||
for i := range _nodes {
|
||||
_nodes[i].Network = network
|
||||
node := ConvertSchemaNodeToModelsNodeWithContext(ctx, &_nodes[i])
|
||||
if node == nil || !IsUserOwnedDevice(node) {
|
||||
continue
|
||||
}
|
||||
out = append(out, *node)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// skipPeerUpdateAclCalc reports whether GetPeerUpdateForHost should skip
|
||||
// FwUpdate ACL calculation. User policies are unidirectional to servers and
|
||||
// are emitted on the server host's peer update instead.
|
||||
|
||||
+31
-23
@@ -21,29 +21,29 @@ type ApiNodeStatus struct {
|
||||
|
||||
// ApiNode is a stripped down Node DTO that exposes only required fields to external systems
|
||||
type ApiNode struct {
|
||||
ID string `json:"id,omitempty" validate:"required,min=5,id_unique"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
HostID string `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
|
||||
Address string `json:"address" validate:"omitempty,cidrv4"`
|
||||
Address6 string `json:"address6" validate:"omitempty,cidrv6"`
|
||||
LocalAddress string `json:"localaddress" validate:"omitempty,cidr"`
|
||||
AllowedIPs []string `json:"allowedips"`
|
||||
LastModified int64 `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
|
||||
ExpirationDateTime int64 `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
|
||||
LastCheckIn int64 `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
|
||||
LastPeerUpdate int64 `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
|
||||
Network string `json:"network"`
|
||||
NetworkRange string `json:"networkrange"`
|
||||
NetworkRange6 string `json:"networkrange6"`
|
||||
IsRelayed bool `json:"isrelayed"`
|
||||
IsRelay bool `json:"isrelay"`
|
||||
IsGw bool `json:"is_gw"`
|
||||
IsAutoRelay bool `json:"is_auto_relay"`
|
||||
AutoRelayedPeers map[string]string `json:"auto_relayed_peers"`
|
||||
AutoAssignGateway bool `json:"auto_assign_gw"`
|
||||
TcpProxyEnabled bool `json:"tcp_proxy_enabled"`
|
||||
TcpProxyListenPort int `json:"tcp_proxy_listen_port"`
|
||||
TcpProxyTLSMode string `json:"tcp_proxy_tls_mode"`
|
||||
ID string `json:"id,omitempty" validate:"required,min=5,id_unique"`
|
||||
TenantID string `json:"tenant_id"`
|
||||
HostID string `json:"hostid,omitempty" validate:"required,min=5,id_unique"`
|
||||
Address string `json:"address" validate:"omitempty,cidrv4"`
|
||||
Address6 string `json:"address6" validate:"omitempty,cidrv6"`
|
||||
LocalAddress string `json:"localaddress" validate:"omitempty,cidr"`
|
||||
AllowedIPs []string `json:"allowedips"`
|
||||
LastModified int64 `json:"lastmodified" swaggertype:"primitive,integer" format:"int64"`
|
||||
ExpirationDateTime int64 `json:"expdatetime" swaggertype:"primitive,integer" format:"int64"`
|
||||
LastCheckIn int64 `json:"lastcheckin" swaggertype:"primitive,integer" format:"int64"`
|
||||
LastPeerUpdate int64 `json:"lastpeerupdate" swaggertype:"primitive,integer" format:"int64"`
|
||||
Network string `json:"network"`
|
||||
NetworkRange string `json:"networkrange"`
|
||||
NetworkRange6 string `json:"networkrange6"`
|
||||
IsRelayed bool `json:"isrelayed"`
|
||||
IsRelay bool `json:"isrelay"`
|
||||
IsGw bool `json:"is_gw"`
|
||||
IsAutoRelay bool `json:"is_auto_relay"`
|
||||
AutoRelayedPeers map[string]string `json:"auto_relayed_peers"`
|
||||
AutoAssignGateway bool `json:"auto_assign_gw"`
|
||||
TcpProxyEnabled bool `json:"tcp_proxy_enabled"`
|
||||
TcpProxyListenPort int `json:"tcp_proxy_listen_port"`
|
||||
TcpProxyTLSMode string `json:"tcp_proxy_tls_mode"`
|
||||
TcpProxyListenAddr string `json:"tcp_proxy_listen_addr,omitempty"`
|
||||
TcpProxyPublicHostname string `json:"tcp_proxy_public_hostname,omitempty"`
|
||||
UseTcpUplink bool `json:"use_tcp_uplink"`
|
||||
@@ -285,6 +285,14 @@ func (nm *Node) ConvertToAPINode() *ApiNode {
|
||||
if apiNode.StaticNode.OwnerID == "" {
|
||||
apiNode.StaticNode.OwnerID = nm.OwnerID
|
||||
}
|
||||
// Non-compliant Users UI reads violations from static_node (ExtClient shape).
|
||||
if len(apiNode.StaticNode.PostureChecksViolations) == 0 && len(nm.PostureChecksViolations) > 0 {
|
||||
apiNode.StaticNode.PostureChecksViolations = nm.PostureChecksViolations
|
||||
apiNode.StaticNode.PostureCheckVolationSeverityLevel = nm.PostureCheckViolationSeverityLevel
|
||||
}
|
||||
if apiNode.StaticNode.LastEvaluatedAt.IsZero() && !nm.LastEvaluatedAt.IsZero() {
|
||||
apiNode.StaticNode.LastEvaluatedAt = nm.LastEvaluatedAt
|
||||
}
|
||||
}
|
||||
return &apiNode
|
||||
}
|
||||
|
||||
@@ -31,6 +31,14 @@ func TestConvertToAPINode_marksUserOwnedHostAsUserNode(t *testing.T) {
|
||||
Status: schema.OnlineSt,
|
||||
},
|
||||
Status: schema.OnlineSt,
|
||||
PostureChecksViolations: []Violation{{
|
||||
CheckID: "loc",
|
||||
Name: "loc",
|
||||
Attribute: "client_location",
|
||||
Message: "client location not allowed",
|
||||
Severity: schema.SeverityMedium,
|
||||
}},
|
||||
PostureCheckViolationSeverityLevel: schema.SeverityMedium,
|
||||
}
|
||||
|
||||
api := nm.ConvertToAPINode()
|
||||
@@ -46,6 +54,12 @@ func TestConvertToAPINode_marksUserOwnedHostAsUserNode(t *testing.T) {
|
||||
if api.StaticNode.DeviceID != hostID.String() {
|
||||
t.Fatalf("expected device_id %s, got %q", hostID, api.StaticNode.DeviceID)
|
||||
}
|
||||
if len(api.StaticNode.PostureChecksViolations) != 1 {
|
||||
t.Fatalf("expected posture violations on static_node for UI, got %d", len(api.StaticNode.PostureChecksViolations))
|
||||
}
|
||||
if api.StaticNode.PostureCheckVolationSeverityLevel != schema.SeverityMedium {
|
||||
t.Fatalf("expected medium severity on static_node, got %v", api.StaticNode.PostureCheckVolationSeverityLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToAPINode_legacyExtClientUnchanged(t *testing.T) {
|
||||
|
||||
@@ -369,6 +369,7 @@ func listPostureCheckViolatedNodes(w http.ResponseWriter, r *http.Request) {
|
||||
listViolatedusers := r.URL.Query().Get("users") == "true"
|
||||
violatedNodes := []models.Node{}
|
||||
if listViolatedusers {
|
||||
// Legacy RAC ExtClients.
|
||||
extclients, err := logic.GetNetworkExtClients(r.Context(), networkName)
|
||||
if err != nil {
|
||||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
||||
@@ -381,6 +382,14 @@ func listPostureCheckViolatedNodes(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Host-backed user devices (desktop) belong under Non-compliant Users,
|
||||
// not Non-compliant Nodes.
|
||||
userDevices, err := logic.ListPostureViolatedUserOwnedDevices(r.Context(), networkName)
|
||||
if err != nil {
|
||||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
||||
return
|
||||
}
|
||||
violatedNodes = append(violatedNodes, userDevices...)
|
||||
} else {
|
||||
network := &schema.Network{
|
||||
Name: networkName,
|
||||
@@ -409,9 +418,16 @@ func listPostureCheckViolatedNodes(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, _node := range _nodes {
|
||||
_node.Network = network
|
||||
node := logic.ConvertSchemaNodeToModelsNode(&_node)
|
||||
for i := range _nodes {
|
||||
_nodes[i].Network = network
|
||||
node := logic.ConvertSchemaNodeToModelsNodeWithContext(r.Context(), &_nodes[i])
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
// User-registered hosts are listed under users=true.
|
||||
if logic.IsUserOwnedDevice(node) {
|
||||
continue
|
||||
}
|
||||
violatedNodes = append(violatedNodes, *node)
|
||||
}
|
||||
}
|
||||
|
||||
+91
-73
@@ -128,7 +128,7 @@ func RunPostureChecksForTenant(ctx context.Context) error {
|
||||
if nodeI.IsStatic && !nodeI.IsUserNode {
|
||||
continue
|
||||
}
|
||||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(ctx, &nodeI)
|
||||
deviceInfo := GetPostureCheckDeviceInfoByNode(ctx, &nodeI)
|
||||
var postureChecksViolations []models.Violation
|
||||
var postureCheckVolationSeverityLevel schema.Severity
|
||||
if noChecks {
|
||||
@@ -245,18 +245,23 @@ func CheckPostureViolationsForHost(
|
||||
network schema.NetworkID,
|
||||
skipAutoUpdate bool,
|
||||
) ([]models.Violation, schema.Severity) {
|
||||
return CheckPostureViolations(ctx, GetPostureCheckDeviceInfoForHost(host, tags, skipAutoUpdate, true), network)
|
||||
return CheckPostureViolations(ctx, GetPostureCheckDeviceInfoForHost(ctx, host, tags, network, skipAutoUpdate, true), network)
|
||||
}
|
||||
|
||||
func GetPostureCheckDeviceInfoForHost(
|
||||
ctx context.Context,
|
||||
host *schema.Host,
|
||||
tags map[models.TagID]struct{},
|
||||
network schema.NetworkID,
|
||||
skipAutoUpdate bool,
|
||||
refreshIntegration bool,
|
||||
) models.PostureCheckDeviceInfo {
|
||||
if host == nil {
|
||||
return models.PostureCheckDeviceInfo{}
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = db.WithContext(context.TODO())
|
||||
}
|
||||
deviceInfo := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: host.CountryCode,
|
||||
ClientVersion: host.Version,
|
||||
@@ -269,12 +274,12 @@ func GetPostureCheckDeviceInfoForHost(
|
||||
Tags: tags,
|
||||
HostID: host.ID.String(),
|
||||
}
|
||||
ctx := db.WithContext(context.TODO())
|
||||
if refreshIntegration {
|
||||
_ = mdmpkg.RefreshHostMDMState(ctx, *host)
|
||||
_ = edrpkg.RefreshHostEDRState(ctx, *host)
|
||||
}
|
||||
attachIntegrationStates(ctx, host.ID.String(), &deviceInfo)
|
||||
attachOwnerUserGroups(ctx, &deviceInfo, host.OwnerUsername, network)
|
||||
return deviceInfo
|
||||
}
|
||||
|
||||
@@ -313,57 +318,8 @@ func GetPostureCheckViolations(ctx context.Context, checks []schema.PostureCheck
|
||||
if c.Attribute == schema.AutoUpdate && (d.IsUser || d.SkipAutoUpdate) {
|
||||
continue
|
||||
}
|
||||
// Check if tags match
|
||||
if !d.IsUser {
|
||||
// Check if posture check has wildcard tag - applies to all devices
|
||||
if _, hasWildcard := c.Tags["*"]; hasWildcard {
|
||||
// Wildcard tag matches all devices, continue to evaluate the check
|
||||
} else if (c.Attribute == schema.MDMCompliance || c.Attribute == schema.EDRCompliance) && len(c.Tags) == 0 {
|
||||
// Legacy MDM/EDR checks saved before wildcard default; apply to all hosts.
|
||||
} else if len(c.Tags) > 0 {
|
||||
// Check has specific tags - device must have at least one matching tag
|
||||
if len(d.Tags) == 0 {
|
||||
// Device has no tags and check doesn't have wildcard, skip
|
||||
continue
|
||||
}
|
||||
exists := false
|
||||
for tagID := range c.Tags {
|
||||
if _, ok := d.Tags[models.TagID(tagID)]; ok {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Check has no tags configured, skip
|
||||
continue
|
||||
}
|
||||
} else if d.IsUser {
|
||||
// Check if posture check has wildcard user group - applies to all users
|
||||
if _, hasWildcard := c.UserGroups["*"]; hasWildcard {
|
||||
// Wildcard user group matches all users, continue to evaluate the check
|
||||
} else if len(c.UserGroups) > 0 {
|
||||
// Check has specific user groups - user must have at least one matching group
|
||||
if len(d.UserGroups) == 0 {
|
||||
// User has no groups and check doesn't have wildcard, skip
|
||||
continue
|
||||
}
|
||||
exists := false
|
||||
for userG := range c.UserGroups {
|
||||
if _, ok := d.UserGroups[schema.UserGroupID(userG)]; ok {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Check has no user groups configured, skip
|
||||
continue
|
||||
}
|
||||
if !postureCheckAppliesToSubject(&c, d) {
|
||||
continue
|
||||
}
|
||||
|
||||
checksByAttribute[c.Attribute] = append(checksByAttribute[c.Attribute], c)
|
||||
@@ -465,6 +421,75 @@ func GetPostureCheckViolations(ctx context.Context, checks []schema.PostureCheck
|
||||
return violations, highest
|
||||
}
|
||||
|
||||
// postureCheckAppliesToSubject reports whether check c is in scope for subject d.
|
||||
// Host-backed subjects may match via Tags and/or UserGroups (OR). ExtClient RAC
|
||||
// subjects (IsUser) only use the UserGroups path.
|
||||
func postureCheckAppliesToSubject(c *schema.PostureCheck, d models.PostureCheckDeviceInfo) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
tagMatch := false
|
||||
userMatch := false
|
||||
|
||||
if !d.IsUser {
|
||||
if _, hasWildcard := c.Tags["*"]; hasWildcard {
|
||||
// Tags "*" applies to every host-backed subject (infra and user devices).
|
||||
tagMatch = true
|
||||
} else if (c.Attribute == schema.MDMCompliance || c.Attribute == schema.EDRCompliance) && len(c.Tags) == 0 {
|
||||
// Legacy MDM/EDR checks saved before wildcard default; apply to all hosts.
|
||||
tagMatch = true
|
||||
} else if len(c.Tags) > 0 {
|
||||
for tagID := range c.Tags {
|
||||
if tagID == "*" {
|
||||
continue
|
||||
}
|
||||
if _, ok := d.Tags[models.TagID(tagID)]; ok {
|
||||
tagMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if d.IsUser || d.Username != "" {
|
||||
if _, hasWildcard := c.UserGroups["*"]; hasWildcard {
|
||||
userMatch = true
|
||||
} else if len(c.UserGroups) > 0 {
|
||||
for userG := range c.UserGroups {
|
||||
if _, ok := d.UserGroups[schema.UserGroupID(userG)]; ok {
|
||||
userMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagMatch || userMatch
|
||||
}
|
||||
|
||||
// attachOwnerUserGroups fills Username / UserGroups for a host-backed or
|
||||
// ExtClient posture subject. Does not set IsUser — host-backed devices keep
|
||||
// the host/device evaluation path (e.g. AutoUpdate).
|
||||
func attachOwnerUserGroups(ctx context.Context, d *models.PostureCheckDeviceInfo, ownerUsername string, network schema.NetworkID) {
|
||||
if d == nil || ownerUsername == "" {
|
||||
return
|
||||
}
|
||||
d.Username = ownerUsername
|
||||
if d.UserGroups == nil {
|
||||
d.UserGroups = make(map[schema.UserGroupID]struct{})
|
||||
}
|
||||
user := &schema.User{Username: ownerUsername}
|
||||
if err := user.GetWithMembership(ctx); err != nil || len(user.UserGroups.Data()) == 0 {
|
||||
return
|
||||
}
|
||||
d.UserGroups = user.UserGroups.Data()
|
||||
if _, ok := user.UserGroups.Data()[GetDefaultGlobalAdminGroupID()]; ok {
|
||||
d.UserGroups[GetDefaultNetworkAdminGroupID(network)] = struct{}{}
|
||||
} else if _, ok := user.UserGroups.Data()[GetDefaultGlobalUserGroupID()]; ok {
|
||||
d.UserGroups[GetDefaultNetworkUserGroupID(network)] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPostureCheckDeviceInfoByNode retrieves PostureCheckDeviceInfo for a given node
|
||||
func GetPostureCheckDeviceInfoByNode(ctx context.Context, node *models.Node) models.PostureCheckDeviceInfo {
|
||||
var deviceInfo models.PostureCheckDeviceInfo
|
||||
@@ -491,6 +516,11 @@ func GetPostureCheckDeviceInfoByNode(ctx context.Context, node *models.Node) mod
|
||||
_ = mdmpkg.RefreshHostMDMState(ctx, *h)
|
||||
_ = edrpkg.RefreshHostEDRState(ctx, *h)
|
||||
attachIntegrationStates(ctx, h.ID.String(), &deviceInfo)
|
||||
owner := h.OwnerUsername
|
||||
if owner == "" {
|
||||
owner = logic.NodeOwnerUsername(node)
|
||||
}
|
||||
attachOwnerUserGroups(ctx, &deviceInfo, owner, schema.NetworkID(node.Network))
|
||||
} else if node.IsUserNode {
|
||||
deviceInfo = models.PostureCheckDeviceInfo{
|
||||
ClientLocation: node.StaticNode.Country,
|
||||
@@ -505,22 +535,7 @@ func GetPostureCheckDeviceInfoByNode(ctx context.Context, node *models.Node) mod
|
||||
ClientID: node.StaticNode.ClientID,
|
||||
UserGroups: make(map[schema.UserGroupID]struct{}),
|
||||
}
|
||||
// get user groups
|
||||
if node.StaticNode.OwnerID != "" {
|
||||
user := &schema.User{Username: node.StaticNode.OwnerID}
|
||||
err := user.GetWithMembership(ctx)
|
||||
if err == nil && len(user.UserGroups.Data()) > 0 {
|
||||
deviceInfo.UserGroups = user.UserGroups.Data()
|
||||
if _, ok := user.UserGroups.Data()[GetDefaultGlobalAdminGroupID()]; ok {
|
||||
|
||||
deviceInfo.UserGroups[GetDefaultNetworkAdminGroupID(schema.NetworkID(node.Network))] = struct{}{}
|
||||
|
||||
} else if _, ok := user.UserGroups.Data()[GetDefaultGlobalUserGroupID()]; ok {
|
||||
|
||||
deviceInfo.UserGroups[GetDefaultNetworkUserGroupID(schema.NetworkID(node.Network))] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachOwnerUserGroups(ctx, &deviceInfo, node.StaticNode.OwnerID, schema.NetworkID(node.Network))
|
||||
}
|
||||
|
||||
return deviceInfo
|
||||
@@ -996,7 +1011,10 @@ func EmitNewPostureViolationEvents(ctx context.Context, oldVi, newVi []models.Vi
|
||||
sourceID := d.HostID
|
||||
sourceName := d.HostID
|
||||
sourceType := schema.DeviceSub
|
||||
if d.IsUser {
|
||||
// ExtClient RAC (IsUser) and host-backed user devices (Username set) are
|
||||
// USER subjects; infra hosts remain DEVICE.
|
||||
isUserSubject := d.IsUser || d.Username != ""
|
||||
if isUserSubject {
|
||||
sourceType = schema.UserSub
|
||||
sourceID = d.Username
|
||||
sourceName = d.Username
|
||||
@@ -1021,9 +1039,9 @@ func EmitNewPostureViolationEvents(ctx context.Context, oldVi, newVi []models.Vi
|
||||
"check": v.Name,
|
||||
"reason": v.Message,
|
||||
"severity": v.Severity,
|
||||
"is_user": d.IsUser,
|
||||
"is_user": isUserSubject,
|
||||
}
|
||||
if d.IsUser {
|
||||
if isUserSubject {
|
||||
payload["username"] = d.Username
|
||||
payload["client_id"] = d.ClientID
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gravitl/netmaker/logic"
|
||||
"github.com/gravitl/netmaker/models"
|
||||
"github.com/gravitl/netmaker/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
func TestPostureCheckAppliesToSubject_allResourcesAndAllUsers(t *testing.T) {
|
||||
check := &schema.PostureCheck{
|
||||
Status: true,
|
||||
Attribute: schema.ClientLocation,
|
||||
Tags: datatypes.JSONMap{"*": "*"},
|
||||
UserGroups: datatypes.JSONMap{"*": "*"},
|
||||
Values: datatypes.JSONSlice[string]{"AX"},
|
||||
}
|
||||
|
||||
infra := models.PostureCheckDeviceInfo{ClientLocation: "US"}
|
||||
assert.True(t, postureCheckAppliesToSubject(check, infra), "Tags=* covers infrastructure")
|
||||
|
||||
userHost := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: "US",
|
||||
Username: "alice",
|
||||
UserGroups: map[schema.UserGroupID]struct{}{"eng": {}},
|
||||
}
|
||||
assert.True(t, postureCheckAppliesToSubject(check, userHost), "UserGroups=* covers host-backed user devices")
|
||||
|
||||
extClient := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: "US",
|
||||
IsUser: true,
|
||||
Username: "bob",
|
||||
UserGroups: map[schema.UserGroupID]struct{}{"eng": {}},
|
||||
}
|
||||
assert.True(t, postureCheckAppliesToSubject(check, extClient), "UserGroups=* covers ExtClient users")
|
||||
}
|
||||
|
||||
func TestPostureCheckAppliesToSubject_tagsStarIncludesUserHosts(t *testing.T) {
|
||||
tagsOnly := &schema.PostureCheck{
|
||||
Status: true,
|
||||
Attribute: schema.ClientLocation,
|
||||
Tags: datatypes.JSONMap{"*": "*"},
|
||||
Values: datatypes.JSONSlice[string]{"AX"},
|
||||
}
|
||||
|
||||
infra := models.PostureCheckDeviceInfo{ClientLocation: "US"}
|
||||
assert.True(t, postureCheckAppliesToSubject(tagsOnly, infra))
|
||||
|
||||
userHost := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: "US",
|
||||
Username: "alice",
|
||||
}
|
||||
assert.True(t, postureCheckAppliesToSubject(tagsOnly, userHost),
|
||||
"Tags=* covers host-backed user devices so All Resources checks still flag them")
|
||||
}
|
||||
|
||||
func TestPostureCheckAppliesToSubject_userGroupsOnly(t *testing.T) {
|
||||
usersOnly := &schema.PostureCheck{
|
||||
Status: true,
|
||||
Attribute: schema.ClientLocation,
|
||||
UserGroups: datatypes.JSONMap{"*": "*"},
|
||||
Values: datatypes.JSONSlice[string]{"AX"},
|
||||
}
|
||||
|
||||
infra := models.PostureCheckDeviceInfo{ClientLocation: "US"}
|
||||
assert.False(t, postureCheckAppliesToSubject(usersOnly, infra))
|
||||
|
||||
userHost := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: "US",
|
||||
Username: "alice",
|
||||
}
|
||||
assert.True(t, postureCheckAppliesToSubject(usersOnly, userHost))
|
||||
}
|
||||
|
||||
func TestGetPostureCheckViolations_flagsHostBackedUserViaUserGroups(t *testing.T) {
|
||||
orig := logic.GetFeatureFlags
|
||||
t.Cleanup(func() { logic.GetFeatureFlags = orig })
|
||||
logic.GetFeatureFlags = func(context.Context) models.FeatureFlags {
|
||||
return models.FeatureFlags{EnablePostureChecks: true}
|
||||
}
|
||||
|
||||
checks := []schema.PostureCheck{{
|
||||
ID: "loc",
|
||||
Name: "loc",
|
||||
Status: true,
|
||||
Attribute: schema.ClientLocation,
|
||||
Severity: schema.SeverityMedium,
|
||||
Tags: datatypes.JSONMap{"*": "*"},
|
||||
UserGroups: datatypes.JSONMap{"*": "*"},
|
||||
Values: datatypes.JSONSlice[string]{"AX"},
|
||||
}}
|
||||
|
||||
userHost := models.PostureCheckDeviceInfo{
|
||||
ClientLocation: "US",
|
||||
Username: "alice",
|
||||
}
|
||||
violations, sev := GetPostureCheckViolations(context.Background(), checks, userHost)
|
||||
require.NotEmpty(t, violations)
|
||||
assert.Equal(t, schema.SeverityMedium, sev)
|
||||
|
||||
infraClean := models.PostureCheckDeviceInfo{ClientLocation: "AX"}
|
||||
violations, sev = GetPostureCheckViolations(context.Background(), checks, infraClean)
|
||||
assert.Empty(t, violations)
|
||||
assert.Equal(t, schema.SeverityUnknown, sev)
|
||||
}
|
||||
Reference in New Issue
Block a user