From 8fa11b1c662d245ad360aa5365f58f684f20a988 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Thu, 28 Dec 2023 15:53:31 +0300 Subject: [PATCH 01/35] wip: add posture checks structs --- management/server/policy.go | 15 ++++++++++----- management/server/posturechecks/checks.go | 12 ++++++++++++ management/server/sqlite_store.go | 3 ++- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 management/server/posturechecks/checks.go diff --git a/management/server/policy.go b/management/server/policy.go index d7e27a1b556..90eb7791bcc 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posturechecks" "github.com/netbirdio/netbird/management/server/status" ) @@ -150,16 +151,20 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` + + // PostureCheck of the policy + PostureCheck posturechecks.PostureCheck `gorm:"foreignKey:PolicyID;references:id"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + PostureCheck: p.PostureCheck, } for i, r := range p.Rules { c.Rules[i] = r.Copy() diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go new file mode 100644 index 00000000000..39f35e91a4a --- /dev/null +++ b/management/server/posturechecks/checks.go @@ -0,0 +1,12 @@ +package posturechecks + +type PostureChecker interface { + Run() (bool, error) +} + +type PostureCheck struct { + ID string `gorm:"primaryKey"` + AccountID string `gorm:"index"` + PolicyID string `gorm:"index"` + Checks []PostureChecker `gorm:"serializer:json"` +} diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 1bc2db3f17f..3e6174fc9ae 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -16,6 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posturechecks" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/route" @@ -63,7 +64,7 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, err = db.AutoMigrate( &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, &account.ExtraSettings{}, + &installation{}, &account.ExtraSettings{}, &posturechecks.PostureCheck{}, ) if err != nil { return nil, err From 92ffd68a65e960b6f067b66f05c4cb798c488fe5 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Thu, 28 Dec 2023 18:13:42 +0300 Subject: [PATCH 02/35] add netbird version check --- management/server/posturechecks/checks.go | 6 +++- management/server/posturechecks/version.go | 36 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 management/server/posturechecks/version.go diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go index 39f35e91a4a..d9a7d8c0d39 100644 --- a/management/server/posturechecks/checks.go +++ b/management/server/posturechecks/checks.go @@ -1,7 +1,11 @@ package posturechecks +import ( + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + type PostureChecker interface { - Run() (bool, error) + Check(peer nbpeer.Peer) error } type PostureCheck struct { diff --git a/management/server/posturechecks/version.go b/management/server/posturechecks/version.go new file mode 100644 index 00000000000..eb7681c5386 --- /dev/null +++ b/management/server/posturechecks/version.go @@ -0,0 +1,36 @@ +package posturechecks + +import ( + "fmt" + + "github.com/hashicorp/go-version" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +type NBVersionCheck struct { + Enabled bool + MinVersion string + MaxVersion string +} + +var _ PostureChecker = (*NBVersionCheck)(nil) + +func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { + peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) + if err != nil { + return err + } + + minMaxVersionRange := ">= " + n.MinVersion + "," + "<= " + n.MaxVersion + constraints, err := version.NewConstraint(minMaxVersionRange) + if err != nil { + return err + } + + if constraints.Check(peerNBVersion) { + return nil + } + + return fmt.Errorf("peer nb version is older than minimum allowed version %s", n.MinVersion) +} From bc2bf4eaa2e44de5e67e000d08eee294cb36174d Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Wed, 3 Jan 2024 14:00:25 +0300 Subject: [PATCH 03/35] Refactor posture checks and add version checks --- management/server/policy.go | 21 +++++---- management/server/posture/checks.go | 45 +++++++++++++++++++ .../{posturechecks => posture}/version.go | 10 +++-- management/server/posturechecks/checks.go | 16 ------- management/server/sqlite_store.go | 4 +- 5 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 management/server/posture/checks.go rename management/server/{posturechecks => posture}/version.go (74%) delete mode 100644 management/server/posturechecks/checks.go diff --git a/management/server/policy.go b/management/server/policy.go index 90eb7791bcc..7f52bd30ec4 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,7 +11,7 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posturechecks" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -152,23 +152,26 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` - // PostureCheck of the policy - PostureCheck posturechecks.PostureCheck `gorm:"foreignKey:PolicyID;references:id"` + // PostureChecks of the policy + PostureChecks []*posture.Checks `gorm:"many2many:policy_posture_checks;"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), - PostureCheck: p.PostureCheck, + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + PostureChecks: make([]*posture.Checks, len(p.PostureChecks)), } for i, r := range p.Rules { c.Rules[i] = r.Copy() } + for i, pc := range p.PostureChecks { + c.PostureChecks[i] = pc.Copy() + } return c } diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go new file mode 100644 index 00000000000..4823f515037 --- /dev/null +++ b/management/server/posture/checks.go @@ -0,0 +1,45 @@ +package posture + +import ( + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +// Check represents an interface for performing a check on a peer. +type Check interface { + Check(peer nbpeer.Peer) error +} + +type Checks struct { + // ID of the posture checks + ID string `gorm:"primaryKey"` + + // Name of the posture checks + Name string + + // Description of the posture checks visible in the UI + Description string + + // AccountID is a reference to the Account that this object belongs + AccountID string `gorm:"index"` + + // Checks is a list of objects that perform the actual checks + Checks []Check `gorm:"serializer:json"` +} + +// TableName returns the name of the table for the Checks model in the database. +func (*Checks) TableName() string { + return "posture_checks" +} + +// Copy returns a copy of a policy rule. +func (pc *Checks) Copy() *Checks { + checks := &Checks{ + ID: pc.ID, + Name: pc.Name, + Description: pc.Description, + AccountID: pc.AccountID, + Checks: make([]Check, len(pc.Checks)), + } + copy(checks.Checks, pc.Checks) + return checks +} diff --git a/management/server/posturechecks/version.go b/management/server/posture/version.go similarity index 74% rename from management/server/posturechecks/version.go rename to management/server/posture/version.go index eb7681c5386..a7e14e843ae 100644 --- a/management/server/posturechecks/version.go +++ b/management/server/posture/version.go @@ -1,4 +1,4 @@ -package posturechecks +package posture import ( "fmt" @@ -14,7 +14,7 @@ type NBVersionCheck struct { MaxVersion string } -var _ PostureChecker = (*NBVersionCheck)(nil) +var _ Check = (*NBVersionCheck)(nil) func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) @@ -32,5 +32,9 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { return nil } - return fmt.Errorf("peer nb version is older than minimum allowed version %s", n.MinVersion) + return fmt.Errorf("peer NB version %s is not within the allowed version range %s to %s", + peer.Meta.UIVersion, + n.MinVersion, + n.MaxVersion, + ) } diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go deleted file mode 100644 index d9a7d8c0d39..00000000000 --- a/management/server/posturechecks/checks.go +++ /dev/null @@ -1,16 +0,0 @@ -package posturechecks - -import ( - nbpeer "github.com/netbirdio/netbird/management/server/peer" -) - -type PostureChecker interface { - Check(peer nbpeer.Peer) error -} - -type PostureCheck struct { - ID string `gorm:"primaryKey"` - AccountID string `gorm:"index"` - PolicyID string `gorm:"index"` - Checks []PostureChecker `gorm:"serializer:json"` -} diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 3e6174fc9ae..578a148327a 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -16,7 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posturechecks" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/route" @@ -64,7 +64,7 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, err = db.AutoMigrate( &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, &account.ExtraSettings{}, &posturechecks.PostureCheck{}, + &installation{}, &account.ExtraSettings{}, &posture.Checks{}, ) if err != nil { return nil, err From 526bff50058203464ca2fb3b2dcb0e0d8ddaa8c4 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 8 Jan 2024 14:34:44 +0300 Subject: [PATCH 04/35] Add posture check activities (#1445) --- management/server/activity/codes.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 54a27e4cc17..9f4fc55589b 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -130,6 +130,12 @@ const ( PeerApprovalRevoked // TransferredOwnerRole indicates that the user transferred the owner role of the account TransferredOwnerRole + // PostureCheckCreated indicates that the user created a posture check + PostureCheckCreated + // PostureCheckUpdated indicates that the user updated a posture check + PostureCheckUpdated + // PostureCheckDeleted indicates that the user deleted a posture check + PostureCheckDeleted ) var activityMap = map[Activity]Code{ @@ -193,6 +199,9 @@ var activityMap = map[Activity]Code{ PeerApproved: {"Peer approved", "peer.approve"}, PeerApprovalRevoked: {"Peer approval revoked", "peer.approval.revoke"}, TransferredOwnerRole: {"Transferred owner role", "transferred.owner.role"}, + PostureCheckCreated: {"Posture check created", "posture.check.created"}, + PostureCheckUpdated: {"Posture check updated", "posture.check.updated"}, + PostureCheckDeleted: {"Posture check deleted", "posture.check.deleted"}, } // StringCode returns a string code of the activity From b2f14271dec71af34cd12749a20c9a0eb25f07c8 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 12 Jan 2024 12:57:27 +0300 Subject: [PATCH 05/35] Integrate Endpoints for Posture Checks (#1432) * wip: add posture checks structs * add netbird version check * Refactor posture checks and add version checks * Implement posture and version checks in API models * Refactor API models and enhance posture check functionality * wip: add posture checks endpoints * go mod tidy * Reference the posture checks by id's in policy * Add posture checks management to server * Add posture checks management mocks * implement posture checks handlers * Add posture checks to account copy and fix tests * Refactor posture checks validation * wip: Add posture checks handler tests * Add JSON encoding support to posture checks * Encode posture checks to correct api response object * Refactored posture checks implementation to align with the new API schema * Refactor structure of `Checks` from slice to map * Cleanup * Add posture check activities (#1445) * Revert map to use list of checks * Add posture check activity events * Refactor posture check initialization in account test * Improve the handling of version range in posture check * Fix tests and linter * Remove max_version from NBVersionCheck * Added unit tests for NBVersionCheck * go mod tidy * Extend policy endpoint with posture checks (#1450) * Implement posture and version checks in API models * go mod tidy * Allow attaching posture checks to policy * Update error message for linked posture check on deleting * Refactor PostureCheck and Checks structures * go mod tidy * Add validation for non-existing posture checks * fix unit tests * use Wt version * Remove the enabled field, as posture check will now automatically be activated by default when attaching to a policy --- go.sum | 2 +- management/server/account.go | 12 + management/server/account_test.go | 16 +- management/server/http/api/openapi.yml | 202 ++++++++++ management/server/http/api/types.gen.go | 51 +++ management/server/http/handler.go | 10 + management/server/http/policies_handler.go | 27 +- .../server/http/posture_checks_handler.go | 232 +++++++++++ .../http/posture_checks_handler_test.go | 360 ++++++++++++++++++ management/server/mock_server/account_mock.go | 39 ++ management/server/policy.go | 21 +- management/server/posture/checks.go | 74 +++- management/server/posture/checks_test.go | 146 +++++++ management/server/posture/version.go | 14 +- management/server/posture/version_test.go | 102 +++++ management/server/posture_checks.go | 144 +++++++ 16 files changed, 1423 insertions(+), 29 deletions(-) create mode 100644 management/server/http/posture_checks_handler.go create mode 100644 management/server/http/posture_checks_handler_test.go create mode 100644 management/server/posture/checks_test.go create mode 100644 management/server/posture/version_test.go create mode 100644 management/server/posture_checks.go diff --git a/go.sum b/go.sum index 175e34c949a..65b84b99fde 100644 --- a/go.sum +++ b/go.sum @@ -996,4 +996,4 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= \ No newline at end of file diff --git a/management/server/account.go b/management/server/account.go index f46f8939faf..fc68466245f 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -30,6 +30,7 @@ import ( "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" ) @@ -118,6 +119,10 @@ type AccountManager interface { GetAllConnectedPeers() (map[string]struct{}, error) HasConnectedChannel(peerID string) bool GetExternalCacheManager() ExternalCacheManager + GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error + DeletePostureChecks(accountID, postureChecksID, userID string) error + ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) } type DefaultAccountManager struct { @@ -216,6 +221,7 @@ type Account struct { NameServerGroups map[string]*nbdns.NameServerGroup `gorm:"-"` NameServerGroupsG []nbdns.NameServerGroup `json:"-" gorm:"foreignKey:AccountID;references:id"` DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"` + PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"` // Settings is a dictionary of Account settings Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` } @@ -661,6 +667,11 @@ func (a *Account) Copy() *Account { settings = a.Settings.Copy() } + postureChecks := []*posture.Checks{} + for _, postureCheck := range a.PostureChecks { + postureChecks = append(postureChecks, postureCheck.Copy()) + } + return &Account{ Id: a.Id, CreatedBy: a.CreatedBy, @@ -677,6 +688,7 @@ func (a *Account) Copy() *Account { Routes: routes, NameServerGroups: nsGroups, DNSSettings: dnsSettings, + PostureChecks: postureChecks, Settings: settings, } } diff --git a/management/server/account_test.go b/management/server/account_test.go index 5e204984e6b..44ca770a74f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -16,6 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/route" "github.com/stretchr/testify/assert" @@ -1537,9 +1538,10 @@ func TestAccount_Copy(t *testing.T) { }, Policies: []*Policy{ { - ID: "policy1", - Enabled: true, - Rules: make([]*PolicyRule, 0), + ID: "policy1", + Enabled: true, + Rules: make([]*PolicyRule, 0), + SourcePostureChecks: make([]string, 0), }, }, Routes: map[string]*route.Route{ @@ -1558,7 +1560,13 @@ func TestAccount_Copy(t *testing.T) { }, }, DNSSettings: DNSSettings{DisabledManagementGroups: []string{}}, - Settings: &Settings{}, + PostureChecks: []*posture.Checks{ + { + ID: "posture Checks1", + Checks: make([]posture.Check, 0), + }, + }, + Settings: &Settings{}, } err := hasNilField(account) if err != nil { diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index cd6b7cbfb0f..e74e06919e0 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -779,6 +779,12 @@ components: - $ref: '#/components/schemas/PolicyMinimum' - type: object properties: + source_posture_checks: + description: Posture checks ID's applied to policy source groups + type: array + items: + type: string + example: "chacdk86lnnboviihd70" rules: description: Policy rule object for policy UI editor type: array @@ -791,6 +797,12 @@ components: - $ref: '#/components/schemas/PolicyMinimum' - type: object properties: + source_posture_checks: + description: Posture checks ID's applied to policy source groups + type: array + items: + type: string + example: "chacdk86lnnboviihd70" rules: description: Policy rule object for policy UI editor type: array @@ -798,6 +810,60 @@ components: $ref: '#/components/schemas/PolicyRule' required: - rules + - source_posture_checks + PostureCheck: + type: object + properties: + id: + description: Posture check ID + type: string + example: ch8i4ug6lnn4g9hqv7mg + name: + description: Posture check name identifier + type: string + example: Default + description: + description: Posture check friendly description + type: string + example: This checks if the peer is running required NetBird's version + checks: + $ref: '#/components/schemas/Checks' + required: + - id + - name + - checks + Checks: + description: List of objects that perform the actual checks + type: object + properties: + nb_version_check: + $ref: '#/components/schemas/NBVersionCheck' + NBVersionCheck: + description: Posture check for the version of NetBird + type: object + properties: + min_version: + description: Minimum acceptable NetBird version + type: string + example: "0.25.0" + required: + - min_version + PostureCheckUpdate: + type: object + properties: + name: + description: Posture check name identifier + type: string + example: Default + description: + description: Posture check friendly description + type: string + example: This checks if the peer is running required NetBird's version + checks: + $ref: '#/components/schemas/Checks' + required: + - name + - description RouteRequest: type: object properties: @@ -2464,3 +2530,139 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/posture-checks: + get: + summary: List all Posture Checks + description: Returns a list of all posture checks + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of posture checks + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Create a Posture Check + description: Creates a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + requestBody: + description: New posture check request + content: + 'application/json': + schema: + $ref: '#/components/schemas/PostureCheckUpdate' + responses: + '200': + description: A posture check Object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + /api/posture-checks/{postureCheckId}: + get: + summary: Retrieve a Posture Check + description: Get information about a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + responses: + '200': + description: A posture check object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update a Posture Check + description: Update/Replace a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + requestBody: + description: Update Rule request + content: + 'application/json': + schema: + $ref: '#/components/schemas/PostureCheckUpdate' + responses: + '200': + description: A posture check object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a Posture Check + description: Delete a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + responses: + '200': + description: Delete status code + content: { } + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" \ No newline at end of file diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 329c6688482..b6291c9f245 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -176,6 +176,12 @@ type AccountSettings struct { PeerLoginExpirationEnabled bool `json:"peer_login_expiration_enabled"` } +// Checks List of objects that perform the actual checks +type Checks struct { + // NbVersionCheck Posture check for the version of NetBird + NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` +} + // DNSSettings defines model for DNSSettings. type DNSSettings struct { // DisabledManagementGroups Groups whose DNS management is disabled @@ -257,6 +263,12 @@ type GroupRequest struct { Peers *[]string `json:"peers,omitempty"` } +// NBVersionCheck Posture check for the version of NetBird +type NBVersionCheck struct { + // MinVersion Minimum acceptable NetBird version + MinVersion string `json:"min_version"` +} + // Nameserver defines model for Nameserver. type Nameserver struct { // Ip Nameserver IP @@ -572,6 +584,9 @@ type Policy struct { // Rules Policy rule object for policy UI editor Rules []PolicyRule `json:"rules"` + + // SourcePostureChecks Posture checks ID's applied to policy source groups + SourcePostureChecks []string `json:"source_posture_checks"` } // PolicyMinimum defines model for PolicyMinimum. @@ -722,6 +737,36 @@ type PolicyUpdate struct { // Rules Policy rule object for policy UI editor Rules []PolicyRuleUpdate `json:"rules"` + + // SourcePostureChecks Posture checks ID's applied to policy source groups + SourcePostureChecks *[]string `json:"source_posture_checks,omitempty"` +} + +// PostureCheck defines model for PostureCheck. +type PostureCheck struct { + // Checks List of objects that perform the actual checks + Checks Checks `json:"checks"` + + // Description Posture check friendly description + Description *string `json:"description,omitempty"` + + // Id Posture check ID + Id string `json:"id"` + + // Name Posture check name identifier + Name string `json:"name"` +} + +// PostureCheckUpdate defines model for PostureCheckUpdate. +type PostureCheckUpdate struct { + // Checks List of objects that perform the actual checks + Checks *Checks `json:"checks,omitempty"` + + // Description Posture check friendly description + Description string `json:"description"` + + // Name Posture check name identifier + Name string `json:"name"` } // Route defines model for Route. @@ -1021,6 +1066,12 @@ type PostApiPoliciesJSONRequestBody = PolicyUpdate // PutApiPoliciesPolicyIdJSONRequestBody defines body for PutApiPoliciesPolicyId for application/json ContentType. type PutApiPoliciesPolicyIdJSONRequestBody = PolicyUpdate +// PostApiPostureChecksJSONRequestBody defines body for PostApiPostureChecks for application/json ContentType. +type PostApiPostureChecksJSONRequestBody = PostureCheckUpdate + +// PutApiPostureChecksPostureCheckIdJSONRequestBody defines body for PutApiPostureChecksPostureCheckId for application/json ContentType. +type PutApiPostureChecksPostureCheckIdJSONRequestBody = PostureCheckUpdate + // PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType. type PostApiRoutesJSONRequestBody = RouteRequest diff --git a/management/server/http/handler.go b/management/server/http/handler.go index c47eac5731f..305af496c7f 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -81,6 +81,7 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid api.addDNSNameserversEndpoint() api.addDNSSettingEndpoint() api.addEventsEndpoint() + api.addPostureCheckEndpoint() err := api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { methods, err := route.GetMethods() @@ -200,3 +201,12 @@ func (apiHandler *apiHandler) addEventsEndpoint() { eventsHandler := NewEventsHandler(apiHandler.AccountManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/events", eventsHandler.GetAllEvents).Methods("GET", "OPTIONS") } + +func (apiHandler *apiHandler) addPostureCheckEndpoint() { + postureCheckHandler := NewPostureChecksHandler(apiHandler.AccountManager, apiHandler.AuthCfg) + apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.GetAllPostureChecks).Methods("GET", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.CreatePostureCheck).Methods("POST", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.UpdatePostureCheck).Methods("PUT", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.GetPostureCheck).Methods("GET", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.DeletePostureCheck).Methods("DELETE", "OPTIONS") +} diff --git a/management/server/http/policies_handler.go b/management/server/http/policies_handler.go index c7b69897af2..e163e63b95e 100644 --- a/management/server/http/policies_handler.go +++ b/management/server/http/policies_handler.go @@ -206,6 +206,10 @@ func (h *Policies) savePolicy( policy.Rules = append(policy.Rules, &pr) } + if req.SourcePostureChecks != nil { + policy.SourcePostureChecks = sourcePostureChecksToStrings(account, *req.SourcePostureChecks) + } + if err := h.accountManager.SavePolicy(account.Id, user.Id, &policy); err != nil { util.WriteError(err, w) return @@ -284,10 +288,11 @@ func (h *Policies) GetPolicy(w http.ResponseWriter, r *http.Request) { func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Policy { cache := make(map[string]api.GroupMinimum) ap := &api.Policy{ - Id: &policy.ID, - Name: policy.Name, - Description: policy.Description, - Enabled: policy.Enabled, + Id: &policy.ID, + Name: policy.Name, + Description: policy.Description, + Enabled: policy.Enabled, + SourcePostureChecks: policy.SourcePostureChecks, } for _, r := range policy.Rules { rID := r.ID @@ -351,3 +356,17 @@ func groupMinimumsToStrings(account *server.Account, gm []string) []string { } return result } + +func sourcePostureChecksToStrings(account *server.Account, postureChecksIds []string) []string { + result := make([]string, 0, len(postureChecksIds)) + for _, id := range postureChecksIds { + for _, postureCheck := range account.PostureChecks { + if id == postureCheck.ID { + result = append(result, id) + continue + } + } + + } + return result +} diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go new file mode 100644 index 00000000000..e40f4a751ca --- /dev/null +++ b/management/server/http/posture_checks_handler.go @@ -0,0 +1,232 @@ +package http + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/xid" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +// PostureChecksHandler is a handler that returns posture checks of the account. +type PostureChecksHandler struct { + accountManager server.AccountManager + claimsExtractor *jwtclaims.ClaimsExtractor +} + +// NewPostureChecksHandler creates a new PostureChecks handler +func NewPostureChecksHandler(accountManager server.AccountManager, authCfg AuthCfg) *PostureChecksHandler { + return &PostureChecksHandler{ + accountManager: accountManager, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithAudience(authCfg.Audience), + jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), + ), + } +} + +// GetAllPostureChecks list for the account +func (p *PostureChecksHandler) GetAllPostureChecks(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + accountPostureChecks, err := p.accountManager.ListPostureChecks(account.Id, user.Id) + if err != nil { + util.WriteError(err, w) + return + } + + postureChecks := []*api.PostureCheck{} + for _, postureCheck := range accountPostureChecks { + postureChecks = append(postureChecks, toPostureChecksResponse(postureCheck)) + } + + util.WriteJSONObject(w, postureChecks) +} + +// UpdatePostureCheck handles update to a posture check identified by a given ID +func (p *PostureChecksHandler) UpdatePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + postureChecksIdx := -1 + for i, postureCheck := range account.PostureChecks { + if postureCheck.ID == postureChecksID { + postureChecksIdx = i + break + } + } + if postureChecksIdx < 0 { + util.WriteError(status.Errorf(status.NotFound, "couldn't find posture checks id %s", postureChecksID), w) + return + } + + p.savePostureChecks(w, r, account, user, postureChecksID) +} + +// CreatePostureCheck handles posture check creation request +func (p *PostureChecksHandler) CreatePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + p.savePostureChecks(w, r, account, user, "") +} + +// GetPostureCheck handles a posture check Get request identified by ID +func (p *PostureChecksHandler) GetPostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + postureChecks, err := p.accountManager.GetPostureChecks(account.Id, postureChecksID, user.Id) + if err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, toPostureChecksResponse(postureChecks)) +} + +// DeletePostureCheck handles posture check deletion request +func (p *PostureChecksHandler) DeletePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + if err = p.accountManager.DeletePostureChecks(account.Id, postureChecksID, user.Id); err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, emptyObject{}) +} + +// savePostureChecks handles posture checks create and update +func (p *PostureChecksHandler) savePostureChecks( + w http.ResponseWriter, + r *http.Request, + account *server.Account, + user *server.User, + postureChecksID string, +) { + + var req api.PostureCheckUpdate + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) + return + } + + err := validatePostureChecksUpdate(req) + if err != nil { + util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) + return + } + + if postureChecksID == "" { + postureChecksID = xid.New().String() + } + + postureChecks := posture.Checks{ + ID: postureChecksID, + Name: req.Name, + Description: req.Description, + Checks: make([]posture.Check, 0), + } + + if nbVersionCheck := req.Checks.NbVersionCheck; nbVersionCheck != nil { + postureChecks.Checks = append(postureChecks.Checks, &posture.NBVersionCheck{ + MinVersion: nbVersionCheck.MinVersion, + }) + + } + + if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, toPostureChecksResponse(&postureChecks)) +} + +func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { + if req.Name == "" { + return status.Errorf(status.InvalidArgument, "posture checks name shouldn't be empty") + } + + if req.Checks == nil || req.Checks.NbVersionCheck == nil { + return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") + } + + if req.Checks.NbVersionCheck != nil && req.Checks.NbVersionCheck.MinVersion == "" { + return status.Errorf(status.InvalidArgument, "minimum version for NetBird's version check shouldn't be empty") + } + + return nil +} + +func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { + var checks api.Checks + for _, check := range postureChecks.Checks { + //nolint:gocritic + switch check.Name() { + case posture.NBVersionCheckName: + versionCheck := check.(*posture.NBVersionCheck) + checks.NbVersionCheck = &api.NBVersionCheck{ + MinVersion: versionCheck.MinVersion, + } + } + } + + return &api.PostureCheck{ + Id: postureChecks.ID, + Name: postureChecks.Name, + Description: &postureChecks.Description, + Checks: checks, + } +} diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go new file mode 100644 index 00000000000..a89d14565d7 --- /dev/null +++ b/management/server/http/posture_checks_handler_test.go @@ -0,0 +1,360 @@ +package http + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksHandler { + testPostureChecks := make(map[string]*posture.Checks, len(postureChecks)) + for _, postureCheck := range postureChecks { + testPostureChecks[postureCheck.ID] = postureCheck + } + + return &PostureChecksHandler{ + accountManager: &mock_server.MockAccountManager{ + GetPostureChecksFunc: func(accountID, postureChecksID, userID string) (*posture.Checks, error) { + p, ok := testPostureChecks[postureChecksID] + if !ok { + return nil, status.Errorf(status.NotFound, "posture checks not found") + } + return p, nil + }, + SavePostureChecksFunc: func(accountID, userID string, postureChecks *posture.Checks) error { + postureChecks.ID = "postureCheck" + testPostureChecks[postureChecks.ID] = postureChecks + return nil + }, + DeletePostureChecksFunc: func(accountID, postureChecksID, userID string) error { + _, ok := testPostureChecks[postureChecksID] + if !ok { + return status.Errorf(status.NotFound, "posture checks not found") + } + delete(testPostureChecks, postureChecksID) + + return nil + }, + ListPostureChecksFunc: func(accountID, userID string) ([]*posture.Checks, error) { + accountPostureChecks := make([]*posture.Checks, len(testPostureChecks)) + for _, p := range testPostureChecks { + accountPostureChecks = append(accountPostureChecks, p) + } + return accountPostureChecks, nil + }, + GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { + user := server.NewAdminUser("test_user") + return &server.Account{ + Id: claims.AccountId, + Users: map[string]*server.User{ + "test_user": user, + }, + PostureChecks: postureChecks, + }, user, nil + }, + }, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { + return jwtclaims.AuthorizationClaims{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + } + }), + ), + } +} + +func TestGetPostureCheck(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "GetPostureCheck OK", + expectedBody: true, + requestType: http.MethodGet, + requestPath: "/api/posture-checks/postureCheck", + expectedStatus: http.StatusOK, + }, + { + name: "GetPostureCheck Not Found", + requestType: http.MethodGet, + requestPath: "/api/posture-checks/not-exists", + expectedStatus: http.StatusNotFound, + }, + } + + postureCheck := &posture.Checks{ + ID: "postureCheck", + Name: "name", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + } + + p := initPostureChecksTestData(postureCheck) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/posture-checks/{postureCheckId}", p.GetPostureCheck).Methods("GET") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, tc.expectedStatus) + return + } + + if !tc.expectedBody { + return + } + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + } + + var got api.PostureCheck + if err = json.Unmarshal(content, &got); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + + assert.Equal(t, got.Id, postureCheck.ID) + assert.Equal(t, got.Name, postureCheck.Name) + }) + } +} + +func TestPostureCheckUpdate(t *testing.T) { + str := func(s string) *string { return &s } + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedPostureCheck *api.PostureCheck + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "Create Posture Checks", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "nb_version_check": { + "min_version": "1.2.3" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + NbVersionCheck: &api.NBVersionCheck{ + MinVersion: "1.2.3", + }, + }, + }, + }, + { + name: "Create Posture Checks Invalid Check", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "non_existing_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Create Posture Checks Invalid Name", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "checks": { + "nb_version_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Create Posture Checks Invalid NetBird's Min Version", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": {} + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": { + "min_version": "1.9.0" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str(""), + Checks: api.Checks{ + NbVersionCheck: &api.NBVersionCheck{ + MinVersion: "1.9.0", + }, + }, + }, + }, + { + name: "Update Posture Checks Invalid Check", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "non_existing_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks Invalid Name", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "checks": { + "nb_version_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks Invalid NetBird's Min Version", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": {} + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + } + + p := initPostureChecksTestData(&posture.Checks{ + ID: "postureCheck", + Name: "postureCheck", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/posture-checks", p.CreatePostureCheck).Methods("POST") + router.HandleFunc("/api/posture-checks/{postureCheckId}", p.UpdatePostureCheck).Methods("PUT") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + return + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if !tc.expectedBody { + return + } + + expected, err := json.Marshal(tc.expectedPostureCheck) + if err != nil { + t.Fatalf("marshal expected posture check: %v", err) + return + } + + assert.Equal(t, strings.Trim(string(content), " \n"), string(expected), "content mismatch") + }) + } +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 4ca68ac216f..f7c3e61df70 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/route" ) @@ -84,6 +85,10 @@ type MockAccountManager struct { GetAllConnectedPeersFunc func() (map[string]struct{}, error) HasConnectedChannelFunc func(peerID string) bool GetExternalCacheManagerFunc func() server.ExternalCacheManager + GetPostureChecksFunc func(accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecksFunc func(accountID, userID string, postureChecks *posture.Checks) error + DeletePostureChecksFunc func(accountID, postureChecksID, userID string) error + ListPostureChecksFunc func(accountID, userID string) ([]*posture.Checks, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -653,3 +658,37 @@ func (am *MockAccountManager) GetExternalCacheManager() server.ExternalCacheMana } return nil } + +// GetPostureChecks mocks GetPostureChecks of the AccountManager interface +func (am *MockAccountManager) GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) { + if am.GetPostureChecksFunc != nil { + return am.GetPostureChecksFunc(accountID, postureChecksID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetPostureChecks is not implemented") + +} + +// SavePostureChecks mocks SavePostureChecks of the AccountManager interface +func (am *MockAccountManager) SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error { + if am.SavePostureChecksFunc != nil { + return am.SavePostureChecksFunc(accountID, userID, postureChecks) + } + return status.Errorf(codes.Unimplemented, "method SavePostureChecks is not implemented") +} + +// DeletePostureChecks mocks DeletePostureChecks of the AccountManager interface +func (am *MockAccountManager) DeletePostureChecks(accountID, postureChecksID, userID string) error { + if am.DeletePostureChecksFunc != nil { + return am.DeletePostureChecksFunc(accountID, postureChecksID, userID) + } + return status.Errorf(codes.Unimplemented, "method DeletePostureChecks is not implemented") + +} + +// ListPostureChecks mocks ListPostureChecks of the AccountManager interface +func (am *MockAccountManager) ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) { + if am.ListPostureChecksFunc != nil { + return am.ListPostureChecksFunc(accountID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method ListPostureChecks is not implemented") +} diff --git a/management/server/policy.go b/management/server/policy.go index 7f52bd30ec4..294d699c796 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,7 +11,6 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -152,26 +151,24 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` - // PostureChecks of the policy - PostureChecks []*posture.Checks `gorm:"many2many:policy_posture_checks;"` + // SourcePostureChecks are ID references to Posture checks for policy source groups + SourcePostureChecks []string `gorm:"serializer:json"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), - PostureChecks: make([]*posture.Checks, len(p.PostureChecks)), + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + SourcePostureChecks: make([]string, len(p.SourcePostureChecks)), } for i, r := range p.Rules { c.Rules[i] = r.Copy() } - for i, pc := range p.PostureChecks { - c.PostureChecks[i] = pc.Copy() - } + copy(c.SourcePostureChecks, p.SourcePostureChecks) return c } diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 4823f515037..a585836d16b 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -1,12 +1,19 @@ package posture import ( + "encoding/json" + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) +const ( + NBVersionCheckName = "NBVersionCheck" +) + // Check represents an interface for performing a check on a peer. type Check interface { Check(peer nbpeer.Peer) error + Name() string } type Checks struct { @@ -20,7 +27,7 @@ type Checks struct { Description string // AccountID is a reference to the Account that this object belongs - AccountID string `gorm:"index"` + AccountID string `json:"-" gorm:"index"` // Checks is a list of objects that perform the actual checks Checks []Check `gorm:"serializer:json"` @@ -43,3 +50,68 @@ func (pc *Checks) Copy() *Checks { copy(checks.Checks, pc.Checks) return checks } + +// EventMeta returns activity event meta-related to this posture checks. +func (pc *Checks) EventMeta() map[string]any { + return map[string]any{"name": pc.Name} +} + +// MarshalJSON returns the JSON encoding of the Checks object. +// The Checks object is marshaled as a map[string]json.RawMessage, +// where the key is the name of the check and the value is the JSON +// representation of the Check object. +func (pc *Checks) MarshalJSON() ([]byte, error) { + type Alias Checks + return json.Marshal(&struct { + Checks map[string]json.RawMessage + *Alias + }{ + Checks: pc.marshalChecks(), + Alias: (*Alias)(pc), + }) +} + +// UnmarshalJSON unmarshal the JSON data into the Checks object. +func (pc *Checks) UnmarshalJSON(data []byte) error { + type Alias Checks + aux := &struct { + Checks map[string]json.RawMessage + *Alias + }{ + Alias: (*Alias)(pc), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + return pc.unmarshalChecks(aux.Checks) +} + +func (pc *Checks) marshalChecks() map[string]json.RawMessage { + result := make(map[string]json.RawMessage) + for _, check := range pc.Checks { + data, err := json.Marshal(check) + if err != nil { + return result + } + result[check.Name()] = data + } + return result +} + +func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { + pc.Checks = make([]Check, 0, len(rawChecks)) + + for name, rawCheck := range rawChecks { + //nolint:gocritic + switch name { + case NBVersionCheckName: + check := &NBVersionCheck{} + if err := json.Unmarshal(rawCheck, check); err != nil { + return err + } + pc.Checks = append(pc.Checks, check) + } + } + return nil +} diff --git a/management/server/posture/checks_test.go b/management/server/posture/checks_test.go new file mode 100644 index 00000000000..cae82caab6a --- /dev/null +++ b/management/server/posture/checks_test.go @@ -0,0 +1,146 @@ +package posture + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChecks_MarshalJSON(t *testing.T) { + tests := []struct { + name string + checks *Checks + want []byte + wantErr bool + }{ + { + name: "Valid Posture Checks Marshal", + checks: &Checks{ + ID: "id1", + Name: "name1", + Description: "desc1", + AccountID: "acc1", + Checks: []Check{ + &NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }, + want: []byte(` + { + "ID": "id1", + "Name": "name1", + "Description": "desc1", + "Checks": { + "NBVersionCheck": { + "MinVersion": "1.0.0" + } + } + } + `), + wantErr: false, + }, + { + name: "Empty Posture Checks Marshal", + checks: &Checks{ + ID: "", + Name: "", + Description: "", + AccountID: "", + Checks: []Check{ + &NBVersionCheck{}, + }, + }, + want: []byte(` + { + "ID": "", + "Name": "", + "Description": "", + "Checks": { + "NBVersionCheck": { + "MinVersion": "" + } + } + } + `), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.checks.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("Checks.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.JSONEq(t, string(got), string(tt.want)) + assert.Equal(t, tt.checks, tt.checks.Copy(), "original Checks should not be modified") + }) + } +} + +func TestChecks_UnmarshalJSON(t *testing.T) { + testCases := []struct { + name string + in []byte + expected *Checks + expectedError bool + }{ + { + name: "Valid JSON Posture Checks Unmarshal", + in: []byte(` + { + "ID": "id1", + "Name": "name1", + "Description": "desc1", + "Checks": { + "NBVersionCheck": { + "Enabled": true, + "MinVersion": "1.0.0" + } + } + } + `), + expected: &Checks{ + ID: "id1", + Name: "name1", + Description: "desc1", + Checks: []Check{ + &NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }, + expectedError: false, + }, + { + name: "Invalid JSON Posture Checks Unmarshal", + in: []byte(`{`), + expectedError: true, + }, + { + name: "Empty JSON Posture Check Unmarshal", + in: []byte(`{}`), + expected: &Checks{ + Checks: make([]Check, 0), + }, + expectedError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checks := &Checks{} + + err := checks.UnmarshalJSON(tc.in) + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, checks) + } + }) + } +} diff --git a/management/server/posture/version.go b/management/server/posture/version.go index a7e14e843ae..46672a967df 100644 --- a/management/server/posture/version.go +++ b/management/server/posture/version.go @@ -9,21 +9,18 @@ import ( ) type NBVersionCheck struct { - Enabled bool MinVersion string - MaxVersion string } var _ Check = (*NBVersionCheck)(nil) func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { - peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) + peerNBVersion, err := version.NewVersion(peer.Meta.WtVersion) if err != nil { return err } - minMaxVersionRange := ">= " + n.MinVersion + "," + "<= " + n.MaxVersion - constraints, err := version.NewConstraint(minMaxVersionRange) + constraints, err := version.NewConstraint(">= " + n.MinVersion) if err != nil { return err } @@ -32,9 +29,12 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { return nil } - return fmt.Errorf("peer NB version %s is not within the allowed version range %s to %s", + return fmt.Errorf("peer NB version %s is older than minimum allowed version %s", peer.Meta.UIVersion, n.MinVersion, - n.MaxVersion, ) } + +func (n *NBVersionCheck) Name() string { + return NBVersionCheckName +} diff --git a/management/server/posture/version_test.go b/management/server/posture/version_test.go new file mode 100644 index 00000000000..c590fd2aeb3 --- /dev/null +++ b/management/server/posture/version_test.go @@ -0,0 +1,102 @@ +package posture + +import ( + "testing" + + "github.com/netbirdio/netbird/management/server/peer" + + "github.com/stretchr/testify/assert" +) + +func TestNBVersionCheck_Check(t *testing.T) { + tests := []struct { + name string + input peer.Peer + check NBVersionCheck + wantErr bool + }{ + { + name: "Valid Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "1.0.1", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "Valid Peer NB version With No Patch Version 1", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "2.0.9", + }, + }, + check: NBVersionCheck{ + MinVersion: "2.0", + }, + wantErr: false, + }, + { + name: "Valid Peer NB version With No Patch Version 2", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "2.0.0", + }, + }, + check: NBVersionCheck{ + MinVersion: "2.0", + }, + wantErr: false, + }, + { + name: "Older Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "0.9.9", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: true, + }, + { + name: "Older Peer NB version With Patch Version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "0.1.0", + }, + }, + check: NBVersionCheck{ + MinVersion: "0.2", + }, + wantErr: true, + }, + { + name: "Invalid Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "x.y.z", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.check.Check(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go new file mode 100644 index 00000000000..0466539fb70 --- /dev/null +++ b/management/server/posture_checks.go @@ -0,0 +1,144 @@ +package server + +import ( + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +func (am *DefaultAccountManager) GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, err + } + + user, err := account.FindUser(userID) + if err != nil { + return nil, err + } + + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + + for _, postureChecks := range account.PostureChecks { + if postureChecks.ID == postureChecksID { + return postureChecks, nil + } + } + + return nil, status.Errorf(status.NotFound, "posture checks with ID %s not found", postureChecksID) +} + +func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + exists := am.savePostureChecks(account, postureChecks) + + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + action := activity.PostureCheckCreated + if exists { + action = activity.PostureCheckUpdated + } + + am.StoreEvent(userID, postureChecks.ID, accountID, action, postureChecks.EventMeta()) + + return nil +} + +func (am *DefaultAccountManager) DeletePostureChecks(accountID, postureChecksID, userID string) error { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + postureChecks, err := am.deletePostureChecks(account, postureChecksID) + if err != nil { + return err + } + + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + am.StoreEvent(userID, postureChecks.ID, accountID, activity.PostureCheckDeleted, postureChecks.EventMeta()) + + return nil +} + +func (am *DefaultAccountManager) ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, err + } + + user, err := account.FindUser(userID) + if err != nil { + return nil, err + } + + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + + return account.PostureChecks, nil +} + +func (am *DefaultAccountManager) savePostureChecks(account *Account, postureChecks *posture.Checks) (exists bool) { + for i, p := range account.PostureChecks { + if p.ID == postureChecks.ID { + account.PostureChecks[i] = postureChecks + exists = true + break + } + } + if !exists { + account.PostureChecks = append(account.PostureChecks, postureChecks) + } + return +} + +func (am *DefaultAccountManager) deletePostureChecks(account *Account, postureChecksID string) (*posture.Checks, error) { + postureChecksIdx := -1 + for i, postureChecks := range account.PostureChecks { + if postureChecks.ID == postureChecksID { + postureChecksIdx = i + break + } + } + if postureChecksIdx < 0 { + return nil, status.Errorf(status.NotFound, "posture checks with ID %s doesn't exist", postureChecksID) + } + + // check policy links + for _, policy := range account.Policies { + for _, id := range policy.SourcePostureChecks { + if id == postureChecksID { + return nil, status.Errorf(status.PreconditionFailed, "posture checks have been linked to policy: %s", policy.Name) + } + } + } + + postureChecks := account.PostureChecks[postureChecksIdx] + account.PostureChecks = append(account.PostureChecks[:postureChecksIdx], account.PostureChecks[postureChecksIdx+1:]...) + + return postureChecks, nil +} From f3d58b9a5a7794a3ccc3a7146af5ae83a493e22a Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Thu, 28 Dec 2023 15:53:31 +0300 Subject: [PATCH 06/35] wip: add posture checks structs --- management/server/policy.go | 15 ++++++++++----- management/server/posturechecks/checks.go | 12 ++++++++++++ management/server/sqlite_store.go | 3 ++- 3 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 management/server/posturechecks/checks.go diff --git a/management/server/policy.go b/management/server/policy.go index d7e27a1b556..90eb7791bcc 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posturechecks" "github.com/netbirdio/netbird/management/server/status" ) @@ -150,16 +151,20 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` + + // PostureCheck of the policy + PostureCheck posturechecks.PostureCheck `gorm:"foreignKey:PolicyID;references:id"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + PostureCheck: p.PostureCheck, } for i, r := range p.Rules { c.Rules[i] = r.Copy() diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go new file mode 100644 index 00000000000..39f35e91a4a --- /dev/null +++ b/management/server/posturechecks/checks.go @@ -0,0 +1,12 @@ +package posturechecks + +type PostureChecker interface { + Run() (bool, error) +} + +type PostureCheck struct { + ID string `gorm:"primaryKey"` + AccountID string `gorm:"index"` + PolicyID string `gorm:"index"` + Checks []PostureChecker `gorm:"serializer:json"` +} diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 1bc2db3f17f..3e6174fc9ae 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -16,6 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posturechecks" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/route" @@ -63,7 +64,7 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, err = db.AutoMigrate( &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, &account.ExtraSettings{}, + &installation{}, &account.ExtraSettings{}, &posturechecks.PostureCheck{}, ) if err != nil { return nil, err From a7ee8c2d884a583d8bafa92914afbfa1e976e8ce Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Thu, 28 Dec 2023 18:13:42 +0300 Subject: [PATCH 07/35] add netbird version check --- management/server/posturechecks/checks.go | 6 +++- management/server/posturechecks/version.go | 36 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 management/server/posturechecks/version.go diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go index 39f35e91a4a..d9a7d8c0d39 100644 --- a/management/server/posturechecks/checks.go +++ b/management/server/posturechecks/checks.go @@ -1,7 +1,11 @@ package posturechecks +import ( + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + type PostureChecker interface { - Run() (bool, error) + Check(peer nbpeer.Peer) error } type PostureCheck struct { diff --git a/management/server/posturechecks/version.go b/management/server/posturechecks/version.go new file mode 100644 index 00000000000..eb7681c5386 --- /dev/null +++ b/management/server/posturechecks/version.go @@ -0,0 +1,36 @@ +package posturechecks + +import ( + "fmt" + + "github.com/hashicorp/go-version" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +type NBVersionCheck struct { + Enabled bool + MinVersion string + MaxVersion string +} + +var _ PostureChecker = (*NBVersionCheck)(nil) + +func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { + peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) + if err != nil { + return err + } + + minMaxVersionRange := ">= " + n.MinVersion + "," + "<= " + n.MaxVersion + constraints, err := version.NewConstraint(minMaxVersionRange) + if err != nil { + return err + } + + if constraints.Check(peerNBVersion) { + return nil + } + + return fmt.Errorf("peer nb version is older than minimum allowed version %s", n.MinVersion) +} From a261cf9b08e646eeeee3044dcadba5e04d5cca32 Mon Sep 17 00:00:00 2001 From: bcmmbaga Date: Wed, 3 Jan 2024 14:00:25 +0300 Subject: [PATCH 08/35] Refactor posture checks and add version checks --- management/server/policy.go | 21 +++++---- management/server/posture/checks.go | 45 +++++++++++++++++++ .../{posturechecks => posture}/version.go | 10 +++-- management/server/posturechecks/checks.go | 16 ------- management/server/sqlite_store.go | 4 +- 5 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 management/server/posture/checks.go rename management/server/{posturechecks => posture}/version.go (74%) delete mode 100644 management/server/posturechecks/checks.go diff --git a/management/server/policy.go b/management/server/policy.go index 90eb7791bcc..7f52bd30ec4 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,7 +11,7 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posturechecks" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -152,23 +152,26 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` - // PostureCheck of the policy - PostureCheck posturechecks.PostureCheck `gorm:"foreignKey:PolicyID;references:id"` + // PostureChecks of the policy + PostureChecks []*posture.Checks `gorm:"many2many:policy_posture_checks;"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), - PostureCheck: p.PostureCheck, + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + PostureChecks: make([]*posture.Checks, len(p.PostureChecks)), } for i, r := range p.Rules { c.Rules[i] = r.Copy() } + for i, pc := range p.PostureChecks { + c.PostureChecks[i] = pc.Copy() + } return c } diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go new file mode 100644 index 00000000000..4823f515037 --- /dev/null +++ b/management/server/posture/checks.go @@ -0,0 +1,45 @@ +package posture + +import ( + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +// Check represents an interface for performing a check on a peer. +type Check interface { + Check(peer nbpeer.Peer) error +} + +type Checks struct { + // ID of the posture checks + ID string `gorm:"primaryKey"` + + // Name of the posture checks + Name string + + // Description of the posture checks visible in the UI + Description string + + // AccountID is a reference to the Account that this object belongs + AccountID string `gorm:"index"` + + // Checks is a list of objects that perform the actual checks + Checks []Check `gorm:"serializer:json"` +} + +// TableName returns the name of the table for the Checks model in the database. +func (*Checks) TableName() string { + return "posture_checks" +} + +// Copy returns a copy of a policy rule. +func (pc *Checks) Copy() *Checks { + checks := &Checks{ + ID: pc.ID, + Name: pc.Name, + Description: pc.Description, + AccountID: pc.AccountID, + Checks: make([]Check, len(pc.Checks)), + } + copy(checks.Checks, pc.Checks) + return checks +} diff --git a/management/server/posturechecks/version.go b/management/server/posture/version.go similarity index 74% rename from management/server/posturechecks/version.go rename to management/server/posture/version.go index eb7681c5386..a7e14e843ae 100644 --- a/management/server/posturechecks/version.go +++ b/management/server/posture/version.go @@ -1,4 +1,4 @@ -package posturechecks +package posture import ( "fmt" @@ -14,7 +14,7 @@ type NBVersionCheck struct { MaxVersion string } -var _ PostureChecker = (*NBVersionCheck)(nil) +var _ Check = (*NBVersionCheck)(nil) func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) @@ -32,5 +32,9 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { return nil } - return fmt.Errorf("peer nb version is older than minimum allowed version %s", n.MinVersion) + return fmt.Errorf("peer NB version %s is not within the allowed version range %s to %s", + peer.Meta.UIVersion, + n.MinVersion, + n.MaxVersion, + ) } diff --git a/management/server/posturechecks/checks.go b/management/server/posturechecks/checks.go deleted file mode 100644 index d9a7d8c0d39..00000000000 --- a/management/server/posturechecks/checks.go +++ /dev/null @@ -1,16 +0,0 @@ -package posturechecks - -import ( - nbpeer "github.com/netbirdio/netbird/management/server/peer" -) - -type PostureChecker interface { - Check(peer nbpeer.Peer) error -} - -type PostureCheck struct { - ID string `gorm:"primaryKey"` - AccountID string `gorm:"index"` - PolicyID string `gorm:"index"` - Checks []PostureChecker `gorm:"serializer:json"` -} diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 3e6174fc9ae..578a148327a 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -16,7 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posturechecks" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/route" @@ -64,7 +64,7 @@ func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, err = db.AutoMigrate( &SetupKey{}, &nbpeer.Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, - &installation{}, &account.ExtraSettings{}, &posturechecks.PostureCheck{}, + &installation{}, &account.ExtraSettings{}, &posture.Checks{}, ) if err != nil { return nil, err From 62fd5afeb5dce3804dfb44b36d3e4165b53c338c Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 8 Jan 2024 14:34:44 +0300 Subject: [PATCH 09/35] Add posture check activities (#1445) --- management/server/activity/codes.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index 54a27e4cc17..9f4fc55589b 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -130,6 +130,12 @@ const ( PeerApprovalRevoked // TransferredOwnerRole indicates that the user transferred the owner role of the account TransferredOwnerRole + // PostureCheckCreated indicates that the user created a posture check + PostureCheckCreated + // PostureCheckUpdated indicates that the user updated a posture check + PostureCheckUpdated + // PostureCheckDeleted indicates that the user deleted a posture check + PostureCheckDeleted ) var activityMap = map[Activity]Code{ @@ -193,6 +199,9 @@ var activityMap = map[Activity]Code{ PeerApproved: {"Peer approved", "peer.approve"}, PeerApprovalRevoked: {"Peer approval revoked", "peer.approval.revoke"}, TransferredOwnerRole: {"Transferred owner role", "transferred.owner.role"}, + PostureCheckCreated: {"Posture check created", "posture.check.created"}, + PostureCheckUpdated: {"Posture check updated", "posture.check.updated"}, + PostureCheckDeleted: {"Posture check deleted", "posture.check.deleted"}, } // StringCode returns a string code of the activity From 9b8340078752ea990b43be65e1312b4524a42c95 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 12 Jan 2024 12:57:27 +0300 Subject: [PATCH 10/35] Integrate Endpoints for Posture Checks (#1432) * wip: add posture checks structs * add netbird version check * Refactor posture checks and add version checks * Implement posture and version checks in API models * Refactor API models and enhance posture check functionality * wip: add posture checks endpoints * go mod tidy * Reference the posture checks by id's in policy * Add posture checks management to server * Add posture checks management mocks * implement posture checks handlers * Add posture checks to account copy and fix tests * Refactor posture checks validation * wip: Add posture checks handler tests * Add JSON encoding support to posture checks * Encode posture checks to correct api response object * Refactored posture checks implementation to align with the new API schema * Refactor structure of `Checks` from slice to map * Cleanup * Add posture check activities (#1445) * Revert map to use list of checks * Add posture check activity events * Refactor posture check initialization in account test * Improve the handling of version range in posture check * Fix tests and linter * Remove max_version from NBVersionCheck * Added unit tests for NBVersionCheck * go mod tidy * Extend policy endpoint with posture checks (#1450) * Implement posture and version checks in API models * go mod tidy * Allow attaching posture checks to policy * Update error message for linked posture check on deleting * Refactor PostureCheck and Checks structures * go mod tidy * Add validation for non-existing posture checks * fix unit tests * use Wt version * Remove the enabled field, as posture check will now automatically be activated by default when attaching to a policy --- go.sum | 2 +- management/server/account.go | 12 + management/server/account_test.go | 16 +- management/server/http/api/openapi.yml | 202 ++++++++++ management/server/http/api/types.gen.go | 51 +++ management/server/http/handler.go | 10 + management/server/http/policies_handler.go | 27 +- .../server/http/posture_checks_handler.go | 232 +++++++++++ .../http/posture_checks_handler_test.go | 360 ++++++++++++++++++ management/server/mock_server/account_mock.go | 39 ++ management/server/policy.go | 21 +- management/server/posture/checks.go | 74 +++- management/server/posture/checks_test.go | 146 +++++++ management/server/posture/version.go | 14 +- management/server/posture/version_test.go | 102 +++++ management/server/posture_checks.go | 144 +++++++ 16 files changed, 1423 insertions(+), 29 deletions(-) create mode 100644 management/server/http/posture_checks_handler.go create mode 100644 management/server/http/posture_checks_handler_test.go create mode 100644 management/server/posture/checks_test.go create mode 100644 management/server/posture/version_test.go create mode 100644 management/server/posture_checks.go diff --git a/go.sum b/go.sum index 3f5ff7511d4..b24b6ddc9e9 100644 --- a/go.sum +++ b/go.sum @@ -1000,4 +1000,4 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= \ No newline at end of file diff --git a/management/server/account.go b/management/server/account.go index 704616bb09f..101790c6389 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -30,6 +30,7 @@ import ( "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" "github.com/netbirdio/netbird/route" ) @@ -119,6 +120,10 @@ type AccountManager interface { GetAllConnectedPeers() (map[string]struct{}, error) HasConnectedChannel(peerID string) bool GetExternalCacheManager() ExternalCacheManager + GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error + DeletePostureChecks(accountID, postureChecksID, userID string) error + ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) } type DefaultAccountManager struct { @@ -217,6 +222,7 @@ type Account struct { NameServerGroups map[string]*nbdns.NameServerGroup `gorm:"-"` NameServerGroupsG []nbdns.NameServerGroup `json:"-" gorm:"foreignKey:AccountID;references:id"` DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"` + PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"` // Settings is a dictionary of Account settings Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` } @@ -662,6 +668,11 @@ func (a *Account) Copy() *Account { settings = a.Settings.Copy() } + postureChecks := []*posture.Checks{} + for _, postureCheck := range a.PostureChecks { + postureChecks = append(postureChecks, postureCheck.Copy()) + } + return &Account{ Id: a.Id, CreatedBy: a.CreatedBy, @@ -678,6 +689,7 @@ func (a *Account) Copy() *Account { Routes: routes, NameServerGroups: nsGroups, DNSSettings: dnsSettings, + PostureChecks: postureChecks, Settings: settings, } } diff --git a/management/server/account_test.go b/management/server/account_test.go index 5e204984e6b..44ca770a74f 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -16,6 +16,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/route" "github.com/stretchr/testify/assert" @@ -1537,9 +1538,10 @@ func TestAccount_Copy(t *testing.T) { }, Policies: []*Policy{ { - ID: "policy1", - Enabled: true, - Rules: make([]*PolicyRule, 0), + ID: "policy1", + Enabled: true, + Rules: make([]*PolicyRule, 0), + SourcePostureChecks: make([]string, 0), }, }, Routes: map[string]*route.Route{ @@ -1558,7 +1560,13 @@ func TestAccount_Copy(t *testing.T) { }, }, DNSSettings: DNSSettings{DisabledManagementGroups: []string{}}, - Settings: &Settings{}, + PostureChecks: []*posture.Checks{ + { + ID: "posture Checks1", + Checks: make([]posture.Check, 0), + }, + }, + Settings: &Settings{}, } err := hasNilField(account) if err != nil { diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index cd6b7cbfb0f..e74e06919e0 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -779,6 +779,12 @@ components: - $ref: '#/components/schemas/PolicyMinimum' - type: object properties: + source_posture_checks: + description: Posture checks ID's applied to policy source groups + type: array + items: + type: string + example: "chacdk86lnnboviihd70" rules: description: Policy rule object for policy UI editor type: array @@ -791,6 +797,12 @@ components: - $ref: '#/components/schemas/PolicyMinimum' - type: object properties: + source_posture_checks: + description: Posture checks ID's applied to policy source groups + type: array + items: + type: string + example: "chacdk86lnnboviihd70" rules: description: Policy rule object for policy UI editor type: array @@ -798,6 +810,60 @@ components: $ref: '#/components/schemas/PolicyRule' required: - rules + - source_posture_checks + PostureCheck: + type: object + properties: + id: + description: Posture check ID + type: string + example: ch8i4ug6lnn4g9hqv7mg + name: + description: Posture check name identifier + type: string + example: Default + description: + description: Posture check friendly description + type: string + example: This checks if the peer is running required NetBird's version + checks: + $ref: '#/components/schemas/Checks' + required: + - id + - name + - checks + Checks: + description: List of objects that perform the actual checks + type: object + properties: + nb_version_check: + $ref: '#/components/schemas/NBVersionCheck' + NBVersionCheck: + description: Posture check for the version of NetBird + type: object + properties: + min_version: + description: Minimum acceptable NetBird version + type: string + example: "0.25.0" + required: + - min_version + PostureCheckUpdate: + type: object + properties: + name: + description: Posture check name identifier + type: string + example: Default + description: + description: Posture check friendly description + type: string + example: This checks if the peer is running required NetBird's version + checks: + $ref: '#/components/schemas/Checks' + required: + - name + - description RouteRequest: type: object properties: @@ -2464,3 +2530,139 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" + /api/posture-checks: + get: + summary: List all Posture Checks + description: Returns a list of all posture checks + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + responses: + '200': + description: A JSON Array of posture checks + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Create a Posture Check + description: Creates a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + requestBody: + description: New posture check request + content: + 'application/json': + schema: + $ref: '#/components/schemas/PostureCheckUpdate' + responses: + '200': + description: A posture check Object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + /api/posture-checks/{postureCheckId}: + get: + summary: Retrieve a Posture Check + description: Get information about a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + responses: + '200': + description: A posture check object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update a Posture Check + description: Update/Replace a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + requestBody: + description: Update Rule request + content: + 'application/json': + schema: + $ref: '#/components/schemas/PostureCheckUpdate' + responses: + '200': + description: A posture check object + content: + application/json: + schema: + $ref: '#/components/schemas/PostureCheck' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a Posture Check + description: Delete a posture check + tags: [ Posture Checks ] + security: + - BearerAuth: [ ] + - TokenAuth: [ ] + parameters: + - in: path + name: postureCheckId + required: true + schema: + type: string + description: The unique identifier of a posture check + responses: + '200': + description: Delete status code + content: { } + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" \ No newline at end of file diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 329c6688482..b6291c9f245 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -176,6 +176,12 @@ type AccountSettings struct { PeerLoginExpirationEnabled bool `json:"peer_login_expiration_enabled"` } +// Checks List of objects that perform the actual checks +type Checks struct { + // NbVersionCheck Posture check for the version of NetBird + NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` +} + // DNSSettings defines model for DNSSettings. type DNSSettings struct { // DisabledManagementGroups Groups whose DNS management is disabled @@ -257,6 +263,12 @@ type GroupRequest struct { Peers *[]string `json:"peers,omitempty"` } +// NBVersionCheck Posture check for the version of NetBird +type NBVersionCheck struct { + // MinVersion Minimum acceptable NetBird version + MinVersion string `json:"min_version"` +} + // Nameserver defines model for Nameserver. type Nameserver struct { // Ip Nameserver IP @@ -572,6 +584,9 @@ type Policy struct { // Rules Policy rule object for policy UI editor Rules []PolicyRule `json:"rules"` + + // SourcePostureChecks Posture checks ID's applied to policy source groups + SourcePostureChecks []string `json:"source_posture_checks"` } // PolicyMinimum defines model for PolicyMinimum. @@ -722,6 +737,36 @@ type PolicyUpdate struct { // Rules Policy rule object for policy UI editor Rules []PolicyRuleUpdate `json:"rules"` + + // SourcePostureChecks Posture checks ID's applied to policy source groups + SourcePostureChecks *[]string `json:"source_posture_checks,omitempty"` +} + +// PostureCheck defines model for PostureCheck. +type PostureCheck struct { + // Checks List of objects that perform the actual checks + Checks Checks `json:"checks"` + + // Description Posture check friendly description + Description *string `json:"description,omitempty"` + + // Id Posture check ID + Id string `json:"id"` + + // Name Posture check name identifier + Name string `json:"name"` +} + +// PostureCheckUpdate defines model for PostureCheckUpdate. +type PostureCheckUpdate struct { + // Checks List of objects that perform the actual checks + Checks *Checks `json:"checks,omitempty"` + + // Description Posture check friendly description + Description string `json:"description"` + + // Name Posture check name identifier + Name string `json:"name"` } // Route defines model for Route. @@ -1021,6 +1066,12 @@ type PostApiPoliciesJSONRequestBody = PolicyUpdate // PutApiPoliciesPolicyIdJSONRequestBody defines body for PutApiPoliciesPolicyId for application/json ContentType. type PutApiPoliciesPolicyIdJSONRequestBody = PolicyUpdate +// PostApiPostureChecksJSONRequestBody defines body for PostApiPostureChecks for application/json ContentType. +type PostApiPostureChecksJSONRequestBody = PostureCheckUpdate + +// PutApiPostureChecksPostureCheckIdJSONRequestBody defines body for PutApiPostureChecksPostureCheckId for application/json ContentType. +type PutApiPostureChecksPostureCheckIdJSONRequestBody = PostureCheckUpdate + // PostApiRoutesJSONRequestBody defines body for PostApiRoutes for application/json ContentType. type PostApiRoutesJSONRequestBody = RouteRequest diff --git a/management/server/http/handler.go b/management/server/http/handler.go index c47eac5731f..305af496c7f 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -81,6 +81,7 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid api.addDNSNameserversEndpoint() api.addDNSSettingEndpoint() api.addEventsEndpoint() + api.addPostureCheckEndpoint() err := api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { methods, err := route.GetMethods() @@ -200,3 +201,12 @@ func (apiHandler *apiHandler) addEventsEndpoint() { eventsHandler := NewEventsHandler(apiHandler.AccountManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/events", eventsHandler.GetAllEvents).Methods("GET", "OPTIONS") } + +func (apiHandler *apiHandler) addPostureCheckEndpoint() { + postureCheckHandler := NewPostureChecksHandler(apiHandler.AccountManager, apiHandler.AuthCfg) + apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.GetAllPostureChecks).Methods("GET", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.CreatePostureCheck).Methods("POST", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.UpdatePostureCheck).Methods("PUT", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.GetPostureCheck).Methods("GET", "OPTIONS") + apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.DeletePostureCheck).Methods("DELETE", "OPTIONS") +} diff --git a/management/server/http/policies_handler.go b/management/server/http/policies_handler.go index c7b69897af2..e163e63b95e 100644 --- a/management/server/http/policies_handler.go +++ b/management/server/http/policies_handler.go @@ -206,6 +206,10 @@ func (h *Policies) savePolicy( policy.Rules = append(policy.Rules, &pr) } + if req.SourcePostureChecks != nil { + policy.SourcePostureChecks = sourcePostureChecksToStrings(account, *req.SourcePostureChecks) + } + if err := h.accountManager.SavePolicy(account.Id, user.Id, &policy); err != nil { util.WriteError(err, w) return @@ -284,10 +288,11 @@ func (h *Policies) GetPolicy(w http.ResponseWriter, r *http.Request) { func toPolicyResponse(account *server.Account, policy *server.Policy) *api.Policy { cache := make(map[string]api.GroupMinimum) ap := &api.Policy{ - Id: &policy.ID, - Name: policy.Name, - Description: policy.Description, - Enabled: policy.Enabled, + Id: &policy.ID, + Name: policy.Name, + Description: policy.Description, + Enabled: policy.Enabled, + SourcePostureChecks: policy.SourcePostureChecks, } for _, r := range policy.Rules { rID := r.ID @@ -351,3 +356,17 @@ func groupMinimumsToStrings(account *server.Account, gm []string) []string { } return result } + +func sourcePostureChecksToStrings(account *server.Account, postureChecksIds []string) []string { + result := make([]string, 0, len(postureChecksIds)) + for _, id := range postureChecksIds { + for _, postureCheck := range account.PostureChecks { + if id == postureCheck.ID { + result = append(result, id) + continue + } + } + + } + return result +} diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go new file mode 100644 index 00000000000..e40f4a751ca --- /dev/null +++ b/management/server/http/posture_checks_handler.go @@ -0,0 +1,232 @@ +package http + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/xid" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +// PostureChecksHandler is a handler that returns posture checks of the account. +type PostureChecksHandler struct { + accountManager server.AccountManager + claimsExtractor *jwtclaims.ClaimsExtractor +} + +// NewPostureChecksHandler creates a new PostureChecks handler +func NewPostureChecksHandler(accountManager server.AccountManager, authCfg AuthCfg) *PostureChecksHandler { + return &PostureChecksHandler{ + accountManager: accountManager, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithAudience(authCfg.Audience), + jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), + ), + } +} + +// GetAllPostureChecks list for the account +func (p *PostureChecksHandler) GetAllPostureChecks(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + accountPostureChecks, err := p.accountManager.ListPostureChecks(account.Id, user.Id) + if err != nil { + util.WriteError(err, w) + return + } + + postureChecks := []*api.PostureCheck{} + for _, postureCheck := range accountPostureChecks { + postureChecks = append(postureChecks, toPostureChecksResponse(postureCheck)) + } + + util.WriteJSONObject(w, postureChecks) +} + +// UpdatePostureCheck handles update to a posture check identified by a given ID +func (p *PostureChecksHandler) UpdatePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + postureChecksIdx := -1 + for i, postureCheck := range account.PostureChecks { + if postureCheck.ID == postureChecksID { + postureChecksIdx = i + break + } + } + if postureChecksIdx < 0 { + util.WriteError(status.Errorf(status.NotFound, "couldn't find posture checks id %s", postureChecksID), w) + return + } + + p.savePostureChecks(w, r, account, user, postureChecksID) +} + +// CreatePostureCheck handles posture check creation request +func (p *PostureChecksHandler) CreatePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + p.savePostureChecks(w, r, account, user, "") +} + +// GetPostureCheck handles a posture check Get request identified by ID +func (p *PostureChecksHandler) GetPostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + postureChecks, err := p.accountManager.GetPostureChecks(account.Id, postureChecksID, user.Id) + if err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, toPostureChecksResponse(postureChecks)) +} + +// DeletePostureCheck handles posture check deletion request +func (p *PostureChecksHandler) DeletePostureCheck(w http.ResponseWriter, r *http.Request) { + claims := p.claimsExtractor.FromRequestContext(r) + account, user, err := p.accountManager.GetAccountFromToken(claims) + if err != nil { + util.WriteError(err, w) + return + } + + vars := mux.Vars(r) + postureChecksID := vars["postureCheckId"] + if len(postureChecksID) == 0 { + util.WriteError(status.Errorf(status.InvalidArgument, "invalid posture checks ID"), w) + return + } + + if err = p.accountManager.DeletePostureChecks(account.Id, postureChecksID, user.Id); err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, emptyObject{}) +} + +// savePostureChecks handles posture checks create and update +func (p *PostureChecksHandler) savePostureChecks( + w http.ResponseWriter, + r *http.Request, + account *server.Account, + user *server.User, + postureChecksID string, +) { + + var req api.PostureCheckUpdate + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + util.WriteErrorResponse("couldn't parse JSON request", http.StatusBadRequest, w) + return + } + + err := validatePostureChecksUpdate(req) + if err != nil { + util.WriteErrorResponse(err.Error(), http.StatusBadRequest, w) + return + } + + if postureChecksID == "" { + postureChecksID = xid.New().String() + } + + postureChecks := posture.Checks{ + ID: postureChecksID, + Name: req.Name, + Description: req.Description, + Checks: make([]posture.Check, 0), + } + + if nbVersionCheck := req.Checks.NbVersionCheck; nbVersionCheck != nil { + postureChecks.Checks = append(postureChecks.Checks, &posture.NBVersionCheck{ + MinVersion: nbVersionCheck.MinVersion, + }) + + } + + if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, toPostureChecksResponse(&postureChecks)) +} + +func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { + if req.Name == "" { + return status.Errorf(status.InvalidArgument, "posture checks name shouldn't be empty") + } + + if req.Checks == nil || req.Checks.NbVersionCheck == nil { + return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") + } + + if req.Checks.NbVersionCheck != nil && req.Checks.NbVersionCheck.MinVersion == "" { + return status.Errorf(status.InvalidArgument, "minimum version for NetBird's version check shouldn't be empty") + } + + return nil +} + +func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { + var checks api.Checks + for _, check := range postureChecks.Checks { + //nolint:gocritic + switch check.Name() { + case posture.NBVersionCheckName: + versionCheck := check.(*posture.NBVersionCheck) + checks.NbVersionCheck = &api.NBVersionCheck{ + MinVersion: versionCheck.MinVersion, + } + } + } + + return &api.PostureCheck{ + Id: postureChecks.ID, + Name: postureChecks.Name, + Description: &postureChecks.Description, + Checks: checks, + } +} diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go new file mode 100644 index 00000000000..a89d14565d7 --- /dev/null +++ b/management/server/http/posture_checks_handler_test.go @@ -0,0 +1,360 @@ +package http + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksHandler { + testPostureChecks := make(map[string]*posture.Checks, len(postureChecks)) + for _, postureCheck := range postureChecks { + testPostureChecks[postureCheck.ID] = postureCheck + } + + return &PostureChecksHandler{ + accountManager: &mock_server.MockAccountManager{ + GetPostureChecksFunc: func(accountID, postureChecksID, userID string) (*posture.Checks, error) { + p, ok := testPostureChecks[postureChecksID] + if !ok { + return nil, status.Errorf(status.NotFound, "posture checks not found") + } + return p, nil + }, + SavePostureChecksFunc: func(accountID, userID string, postureChecks *posture.Checks) error { + postureChecks.ID = "postureCheck" + testPostureChecks[postureChecks.ID] = postureChecks + return nil + }, + DeletePostureChecksFunc: func(accountID, postureChecksID, userID string) error { + _, ok := testPostureChecks[postureChecksID] + if !ok { + return status.Errorf(status.NotFound, "posture checks not found") + } + delete(testPostureChecks, postureChecksID) + + return nil + }, + ListPostureChecksFunc: func(accountID, userID string) ([]*posture.Checks, error) { + accountPostureChecks := make([]*posture.Checks, len(testPostureChecks)) + for _, p := range testPostureChecks { + accountPostureChecks = append(accountPostureChecks, p) + } + return accountPostureChecks, nil + }, + GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { + user := server.NewAdminUser("test_user") + return &server.Account{ + Id: claims.AccountId, + Users: map[string]*server.User{ + "test_user": user, + }, + PostureChecks: postureChecks, + }, user, nil + }, + }, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { + return jwtclaims.AuthorizationClaims{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + } + }), + ), + } +} + +func TestGetPostureCheck(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "GetPostureCheck OK", + expectedBody: true, + requestType: http.MethodGet, + requestPath: "/api/posture-checks/postureCheck", + expectedStatus: http.StatusOK, + }, + { + name: "GetPostureCheck Not Found", + requestType: http.MethodGet, + requestPath: "/api/posture-checks/not-exists", + expectedStatus: http.StatusNotFound, + }, + } + + postureCheck := &posture.Checks{ + ID: "postureCheck", + Name: "name", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + } + + p := initPostureChecksTestData(postureCheck) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/posture-checks/{postureCheckId}", p.GetPostureCheck).Methods("GET") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v", + status, tc.expectedStatus) + return + } + + if !tc.expectedBody { + return + } + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + } + + var got api.PostureCheck + if err = json.Unmarshal(content, &got); err != nil { + t.Fatalf("Sent content is not in correct json format; %v", err) + } + + assert.Equal(t, got.Id, postureCheck.ID) + assert.Equal(t, got.Name, postureCheck.Name) + }) + } +} + +func TestPostureCheckUpdate(t *testing.T) { + str := func(s string) *string { return &s } + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedPostureCheck *api.PostureCheck + requestType string + requestPath string + requestBody io.Reader + }{ + { + name: "Create Posture Checks", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "nb_version_check": { + "min_version": "1.2.3" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + NbVersionCheck: &api.NBVersionCheck{ + MinVersion: "1.2.3", + }, + }, + }, + }, + { + name: "Create Posture Checks Invalid Check", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "non_existing_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Create Posture Checks Invalid Name", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "checks": { + "nb_version_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Create Posture Checks Invalid NetBird's Min Version", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": {} + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": { + "min_version": "1.9.0" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str(""), + Checks: api.Checks{ + NbVersionCheck: &api.NBVersionCheck{ + MinVersion: "1.9.0", + }, + }, + }, + }, + { + name: "Update Posture Checks Invalid Check", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "non_existing_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks Invalid Name", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "checks": { + "nb_version_check": { + "min_version": "1.2.0" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Update Posture Checks Invalid NetBird's Min Version", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/postureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "nb_version_check": {} + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + } + + p := initPostureChecksTestData(&posture.Checks{ + ID: "postureCheck", + Name: "postureCheck", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + + router := mux.NewRouter() + router.HandleFunc("/api/posture-checks", p.CreatePostureCheck).Methods("POST") + router.HandleFunc("/api/posture-checks/{postureCheckId}", p.UpdatePostureCheck).Methods("PUT") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + return + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if !tc.expectedBody { + return + } + + expected, err := json.Marshal(tc.expectedPostureCheck) + if err != nil { + t.Fatalf("marshal expected posture check: %v", err) + return + } + + assert.Equal(t, strings.Trim(string(content), " \n"), string(expected), "content mismatch") + }) + } +} diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index f337ef1cfb8..94abf8124e0 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/route" ) @@ -85,6 +86,10 @@ type MockAccountManager struct { GetAllConnectedPeersFunc func() (map[string]struct{}, error) HasConnectedChannelFunc func(peerID string) bool GetExternalCacheManagerFunc func() server.ExternalCacheManager + GetPostureChecksFunc func(accountID, postureChecksID, userID string) (*posture.Checks, error) + SavePostureChecksFunc func(accountID, userID string, postureChecks *posture.Checks) error + DeletePostureChecksFunc func(accountID, postureChecksID, userID string) error + ListPostureChecksFunc func(accountID, userID string) ([]*posture.Checks, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -662,3 +667,37 @@ func (am *MockAccountManager) GetExternalCacheManager() server.ExternalCacheMana } return nil } + +// GetPostureChecks mocks GetPostureChecks of the AccountManager interface +func (am *MockAccountManager) GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) { + if am.GetPostureChecksFunc != nil { + return am.GetPostureChecksFunc(accountID, postureChecksID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetPostureChecks is not implemented") + +} + +// SavePostureChecks mocks SavePostureChecks of the AccountManager interface +func (am *MockAccountManager) SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error { + if am.SavePostureChecksFunc != nil { + return am.SavePostureChecksFunc(accountID, userID, postureChecks) + } + return status.Errorf(codes.Unimplemented, "method SavePostureChecks is not implemented") +} + +// DeletePostureChecks mocks DeletePostureChecks of the AccountManager interface +func (am *MockAccountManager) DeletePostureChecks(accountID, postureChecksID, userID string) error { + if am.DeletePostureChecksFunc != nil { + return am.DeletePostureChecksFunc(accountID, postureChecksID, userID) + } + return status.Errorf(codes.Unimplemented, "method DeletePostureChecks is not implemented") + +} + +// ListPostureChecks mocks ListPostureChecks of the AccountManager interface +func (am *MockAccountManager) ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) { + if am.ListPostureChecksFunc != nil { + return am.ListPostureChecksFunc(accountID, userID) + } + return nil, status.Errorf(codes.Unimplemented, "method ListPostureChecks is not implemented") +} diff --git a/management/server/policy.go b/management/server/policy.go index 7f52bd30ec4..294d699c796 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,7 +11,6 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" - "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -152,26 +151,24 @@ type Policy struct { // Rules of the policy Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` - // PostureChecks of the policy - PostureChecks []*posture.Checks `gorm:"many2many:policy_posture_checks;"` + // SourcePostureChecks are ID references to Posture checks for policy source groups + SourcePostureChecks []string `gorm:"serializer:json"` } // Copy returns a copy of the policy. func (p *Policy) Copy() *Policy { c := &Policy{ - ID: p.ID, - Name: p.Name, - Description: p.Description, - Enabled: p.Enabled, - Rules: make([]*PolicyRule, len(p.Rules)), - PostureChecks: make([]*posture.Checks, len(p.PostureChecks)), + ID: p.ID, + Name: p.Name, + Description: p.Description, + Enabled: p.Enabled, + Rules: make([]*PolicyRule, len(p.Rules)), + SourcePostureChecks: make([]string, len(p.SourcePostureChecks)), } for i, r := range p.Rules { c.Rules[i] = r.Copy() } - for i, pc := range p.PostureChecks { - c.PostureChecks[i] = pc.Copy() - } + copy(c.SourcePostureChecks, p.SourcePostureChecks) return c } diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 4823f515037..a585836d16b 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -1,12 +1,19 @@ package posture import ( + "encoding/json" + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) +const ( + NBVersionCheckName = "NBVersionCheck" +) + // Check represents an interface for performing a check on a peer. type Check interface { Check(peer nbpeer.Peer) error + Name() string } type Checks struct { @@ -20,7 +27,7 @@ type Checks struct { Description string // AccountID is a reference to the Account that this object belongs - AccountID string `gorm:"index"` + AccountID string `json:"-" gorm:"index"` // Checks is a list of objects that perform the actual checks Checks []Check `gorm:"serializer:json"` @@ -43,3 +50,68 @@ func (pc *Checks) Copy() *Checks { copy(checks.Checks, pc.Checks) return checks } + +// EventMeta returns activity event meta-related to this posture checks. +func (pc *Checks) EventMeta() map[string]any { + return map[string]any{"name": pc.Name} +} + +// MarshalJSON returns the JSON encoding of the Checks object. +// The Checks object is marshaled as a map[string]json.RawMessage, +// where the key is the name of the check and the value is the JSON +// representation of the Check object. +func (pc *Checks) MarshalJSON() ([]byte, error) { + type Alias Checks + return json.Marshal(&struct { + Checks map[string]json.RawMessage + *Alias + }{ + Checks: pc.marshalChecks(), + Alias: (*Alias)(pc), + }) +} + +// UnmarshalJSON unmarshal the JSON data into the Checks object. +func (pc *Checks) UnmarshalJSON(data []byte) error { + type Alias Checks + aux := &struct { + Checks map[string]json.RawMessage + *Alias + }{ + Alias: (*Alias)(pc), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + return pc.unmarshalChecks(aux.Checks) +} + +func (pc *Checks) marshalChecks() map[string]json.RawMessage { + result := make(map[string]json.RawMessage) + for _, check := range pc.Checks { + data, err := json.Marshal(check) + if err != nil { + return result + } + result[check.Name()] = data + } + return result +} + +func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { + pc.Checks = make([]Check, 0, len(rawChecks)) + + for name, rawCheck := range rawChecks { + //nolint:gocritic + switch name { + case NBVersionCheckName: + check := &NBVersionCheck{} + if err := json.Unmarshal(rawCheck, check); err != nil { + return err + } + pc.Checks = append(pc.Checks, check) + } + } + return nil +} diff --git a/management/server/posture/checks_test.go b/management/server/posture/checks_test.go new file mode 100644 index 00000000000..cae82caab6a --- /dev/null +++ b/management/server/posture/checks_test.go @@ -0,0 +1,146 @@ +package posture + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChecks_MarshalJSON(t *testing.T) { + tests := []struct { + name string + checks *Checks + want []byte + wantErr bool + }{ + { + name: "Valid Posture Checks Marshal", + checks: &Checks{ + ID: "id1", + Name: "name1", + Description: "desc1", + AccountID: "acc1", + Checks: []Check{ + &NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }, + want: []byte(` + { + "ID": "id1", + "Name": "name1", + "Description": "desc1", + "Checks": { + "NBVersionCheck": { + "MinVersion": "1.0.0" + } + } + } + `), + wantErr: false, + }, + { + name: "Empty Posture Checks Marshal", + checks: &Checks{ + ID: "", + Name: "", + Description: "", + AccountID: "", + Checks: []Check{ + &NBVersionCheck{}, + }, + }, + want: []byte(` + { + "ID": "", + "Name": "", + "Description": "", + "Checks": { + "NBVersionCheck": { + "MinVersion": "" + } + } + } + `), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.checks.MarshalJSON() + if (err != nil) != tt.wantErr { + t.Errorf("Checks.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.JSONEq(t, string(got), string(tt.want)) + assert.Equal(t, tt.checks, tt.checks.Copy(), "original Checks should not be modified") + }) + } +} + +func TestChecks_UnmarshalJSON(t *testing.T) { + testCases := []struct { + name string + in []byte + expected *Checks + expectedError bool + }{ + { + name: "Valid JSON Posture Checks Unmarshal", + in: []byte(` + { + "ID": "id1", + "Name": "name1", + "Description": "desc1", + "Checks": { + "NBVersionCheck": { + "Enabled": true, + "MinVersion": "1.0.0" + } + } + } + `), + expected: &Checks{ + ID: "id1", + Name: "name1", + Description: "desc1", + Checks: []Check{ + &NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + }, + expectedError: false, + }, + { + name: "Invalid JSON Posture Checks Unmarshal", + in: []byte(`{`), + expectedError: true, + }, + { + name: "Empty JSON Posture Check Unmarshal", + in: []byte(`{}`), + expected: &Checks{ + Checks: make([]Check, 0), + }, + expectedError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + checks := &Checks{} + + err := checks.UnmarshalJSON(tc.in) + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, checks) + } + }) + } +} diff --git a/management/server/posture/version.go b/management/server/posture/version.go index a7e14e843ae..46672a967df 100644 --- a/management/server/posture/version.go +++ b/management/server/posture/version.go @@ -9,21 +9,18 @@ import ( ) type NBVersionCheck struct { - Enabled bool MinVersion string - MaxVersion string } var _ Check = (*NBVersionCheck)(nil) func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { - peerNBVersion, err := version.NewVersion(peer.Meta.UIVersion) + peerNBVersion, err := version.NewVersion(peer.Meta.WtVersion) if err != nil { return err } - minMaxVersionRange := ">= " + n.MinVersion + "," + "<= " + n.MaxVersion - constraints, err := version.NewConstraint(minMaxVersionRange) + constraints, err := version.NewConstraint(">= " + n.MinVersion) if err != nil { return err } @@ -32,9 +29,12 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { return nil } - return fmt.Errorf("peer NB version %s is not within the allowed version range %s to %s", + return fmt.Errorf("peer NB version %s is older than minimum allowed version %s", peer.Meta.UIVersion, n.MinVersion, - n.MaxVersion, ) } + +func (n *NBVersionCheck) Name() string { + return NBVersionCheckName +} diff --git a/management/server/posture/version_test.go b/management/server/posture/version_test.go new file mode 100644 index 00000000000..c590fd2aeb3 --- /dev/null +++ b/management/server/posture/version_test.go @@ -0,0 +1,102 @@ +package posture + +import ( + "testing" + + "github.com/netbirdio/netbird/management/server/peer" + + "github.com/stretchr/testify/assert" +) + +func TestNBVersionCheck_Check(t *testing.T) { + tests := []struct { + name string + input peer.Peer + check NBVersionCheck + wantErr bool + }{ + { + name: "Valid Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "1.0.1", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "Valid Peer NB version With No Patch Version 1", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "2.0.9", + }, + }, + check: NBVersionCheck{ + MinVersion: "2.0", + }, + wantErr: false, + }, + { + name: "Valid Peer NB version With No Patch Version 2", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "2.0.0", + }, + }, + check: NBVersionCheck{ + MinVersion: "2.0", + }, + wantErr: false, + }, + { + name: "Older Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "0.9.9", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: true, + }, + { + name: "Older Peer NB version With Patch Version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "0.1.0", + }, + }, + check: NBVersionCheck{ + MinVersion: "0.2", + }, + wantErr: true, + }, + { + name: "Invalid Peer NB version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + WtVersion: "x.y.z", + }, + }, + check: NBVersionCheck{ + MinVersion: "1.0.0", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.check.Check(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go new file mode 100644 index 00000000000..0466539fb70 --- /dev/null +++ b/management/server/posture_checks.go @@ -0,0 +1,144 @@ +package server + +import ( + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/management/server/status" +) + +func (am *DefaultAccountManager) GetPostureChecks(accountID, postureChecksID, userID string) (*posture.Checks, error) { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, err + } + + user, err := account.FindUser(userID) + if err != nil { + return nil, err + } + + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + + for _, postureChecks := range account.PostureChecks { + if postureChecks.ID == postureChecksID { + return postureChecks, nil + } + } + + return nil, status.Errorf(status.NotFound, "posture checks with ID %s not found", postureChecksID) +} + +func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, postureChecks *posture.Checks) error { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + exists := am.savePostureChecks(account, postureChecks) + + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + action := activity.PostureCheckCreated + if exists { + action = activity.PostureCheckUpdated + } + + am.StoreEvent(userID, postureChecks.ID, accountID, action, postureChecks.EventMeta()) + + return nil +} + +func (am *DefaultAccountManager) DeletePostureChecks(accountID, postureChecksID, userID string) error { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return err + } + + postureChecks, err := am.deletePostureChecks(account, postureChecksID) + if err != nil { + return err + } + + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + am.StoreEvent(userID, postureChecks.ID, accountID, activity.PostureCheckDeleted, postureChecks.EventMeta()) + + return nil +} + +func (am *DefaultAccountManager) ListPostureChecks(accountID, userID string) ([]*posture.Checks, error) { + unlock := am.Store.AcquireAccountLock(accountID) + defer unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, err + } + + user, err := account.FindUser(userID) + if err != nil { + return nil, err + } + + if !user.HasAdminPower() { + return nil, status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + + return account.PostureChecks, nil +} + +func (am *DefaultAccountManager) savePostureChecks(account *Account, postureChecks *posture.Checks) (exists bool) { + for i, p := range account.PostureChecks { + if p.ID == postureChecks.ID { + account.PostureChecks[i] = postureChecks + exists = true + break + } + } + if !exists { + account.PostureChecks = append(account.PostureChecks, postureChecks) + } + return +} + +func (am *DefaultAccountManager) deletePostureChecks(account *Account, postureChecksID string) (*posture.Checks, error) { + postureChecksIdx := -1 + for i, postureChecks := range account.PostureChecks { + if postureChecks.ID == postureChecksID { + postureChecksIdx = i + break + } + } + if postureChecksIdx < 0 { + return nil, status.Errorf(status.NotFound, "posture checks with ID %s doesn't exist", postureChecksID) + } + + // check policy links + for _, policy := range account.Policies { + for _, id := range policy.SourcePostureChecks { + if id == postureChecksID { + return nil, status.Errorf(status.PreconditionFailed, "posture checks have been linked to policy: %s", policy.Name) + } + } + } + + postureChecks := account.PostureChecks[postureChecksIdx] + account.PostureChecks = append(account.PostureChecks[:postureChecksIdx], account.PostureChecks[postureChecksIdx+1:]...) + + return postureChecks, nil +} From 3604a97c6d53460346af376b63ee016055575a67 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 22 Jan 2024 18:23:53 +0300 Subject: [PATCH 11/35] Extend network map generation with posture checks (#1466) * Apply posture checks to network map generation * run policy posture checks on peers to connect * Refactor and streamline policy posture check process for peers to connect. * Add posture checks testing in a network map * Remove redundant nil check in policy.go * Refactor peer validation check in policy.go * Update 'Check' function signature and use logger for version check * Refactor posture checks run on sources and updated the validation func * Update peer validation * fix tests * improved test coverage for policy posture check * Refactoring --- management/server/policy.go | 55 ++++- management/server/policy_test.go | 269 ++++++++++++++++++++++ management/server/posture/checks.go | 2 +- management/server/posture/version.go | 15 +- management/server/posture/version_test.go | 14 +- 5 files changed, 340 insertions(+), 15 deletions(-) diff --git a/management/server/policy.go b/management/server/policy.go index 294d699c796..050e8d60d7f 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -11,6 +11,7 @@ import ( "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server/activity" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/status" ) @@ -224,8 +225,8 @@ func (a *Account) getPeerConnectionResources(peerID string) ([]*nbpeer.Peer, []* continue } - sourcePeers, peerInSources := getAllPeersFromGroups(a, rule.Sources, peerID) - destinationPeers, peerInDestinations := getAllPeersFromGroups(a, rule.Destinations, peerID) + sourcePeers, peerInSources := getAllPeersFromGroups(a, rule.Sources, peerID, policy.SourcePostureChecks) + destinationPeers, peerInDestinations := getAllPeersFromGroups(a, rule.Destinations, peerID, nil) sourcePeers = additions.ValidatePeers(sourcePeers) destinationPeers = additions.ValidatePeers(destinationPeers) @@ -274,6 +275,7 @@ func (a *Account) connResourcesGenerator() (func(*PolicyRule, []*nbpeer.Peer, in if peer == nil { continue } + if _, ok := peersExists[peer.ID]; !ok { peers = append(peers, peer) peersExists[peer.ID] = struct{}{} @@ -486,8 +488,12 @@ func toProtocolFirewallRules(update []*FirewallRule) []*proto.FirewallRule { // getAllPeersFromGroups for given peer ID and list of groups // -// Returns list of peers and boolean indicating if peer is in any of the groups -func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([]*nbpeer.Peer, bool) { +// Returns a list of peers from specified groups that pass specified posture checks +// and a boolean indicating if the supplied peer ID exists within these groups. +// +// Important: Posture checks are applicable only to source group peers, +// for destination group peers, call this method with an empty list of sourcePostureChecksIDs +func getAllPeersFromGroups(account *Account, groups []string, peerID string, sourcePostureChecksIDs []string) ([]*nbpeer.Peer, bool) { peerInGroups := false filteredPeers := make([]*nbpeer.Peer, 0, len(groups)) for _, g := range groups { @@ -502,6 +508,12 @@ func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([] continue } + // validate the peer based on policy posture checks applied + isValid := account.validatePostureChecksOnPeer(sourcePostureChecksIDs, peer.ID) + if !isValid { + continue + } + if peer.ID == peerID { peerInGroups = true continue @@ -512,3 +524,38 @@ func getAllPeersFromGroups(account *Account, groups []string, peerID string) ([] } return filteredPeers, peerInGroups } + +// validatePostureChecksOnPeer validates the posture checks on a peer +func (a *Account) validatePostureChecksOnPeer(sourcePostureChecksID []string, peerID string) bool { + peer, ok := a.Peers[peerID] + if !ok && peer == nil { + return false + } + + for _, postureChecksID := range sourcePostureChecksID { + postureChecks := getPostureChecks(a, postureChecksID) + if postureChecks == nil { + continue + } + + for _, check := range postureChecks.Checks { + isValid, err := check.Check(*peer) + if err != nil { + log.Debugf("an error occurred check %s: on peer: %s :%s", check.Name(), peer.ID, err.Error()) + } + if !isValid { + return false + } + } + } + return true +} + +func getPostureChecks(account *Account, postureChecksID string) *posture.Checks { + for _, postureChecks := range account.PostureChecks { + if postureChecks.ID == postureChecksID { + return postureChecks + } + } + return nil +} diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 715e2a8614e..d80841bde96 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -9,6 +9,7 @@ import ( "golang.org/x/exp/slices" nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/posture" ) func TestAccount_getPeersByPolicy(t *testing.T) { @@ -443,6 +444,274 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { }) } +func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { + account := &Account{ + Peers: map[string]*nbpeer.Peer{ + "peerA": { + ID: "peerA", + IP: net.ParseIP("100.65.14.88"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.25.9", + }, + }, + "peerB": { + ID: "peerB", + IP: net.ParseIP("100.65.80.39"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.23.0", + }, + }, + "peerC": { + ID: "peerC", + IP: net.ParseIP("100.65.254.139"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.25.8", + }, + }, + "peerD": { + ID: "peerD", + IP: net.ParseIP("100.65.62.5"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.25.9", + }, + }, + "peerE": { + ID: "peerE", + IP: net.ParseIP("100.65.32.206"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.24.0", + }, + }, + "peerF": { + ID: "peerF", + IP: net.ParseIP("100.65.250.202"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.25.9", + }, + }, + "peerG": { + ID: "peerG", + IP: net.ParseIP("100.65.13.186"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.23.2", + }, + }, + "peerH": { + ID: "peerH", + IP: net.ParseIP("100.65.29.55"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + WtVersion: "0.23.1", + }, + }, + }, + Groups: map[string]*Group{ + "GroupAll": { + ID: "GroupAll", + Name: "All", + Peers: []string{ + "peerB", + "peerA", + "peerD", + "peerC", + "peerF", + "peerG", + "peerH", + }, + }, + "GroupSwarm": { + ID: "GroupSwarm", + Name: "swarm", + Peers: []string{ + "peerB", + "peerA", + "peerD", + "peerE", + "peerG", + "peerH", + }, + }, + }, + PostureChecks: []*posture.Checks{ + { + ID: "PostureChecksDefault", + Name: "Default", + Description: "This is a posture checks that check if peer is running required version", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "0.25", + }, + }, + }, + }, + } + + account.Policies = append(account.Policies, &Policy{ + ID: "PolicyPostureChecks", + Name: "", + Description: "This is the policy with posture checks applied", + Enabled: true, + Rules: []*PolicyRule{ + { + ID: "RuleSwarm", + Name: "Swarm", + Enabled: true, + Action: PolicyTrafficActionAccept, + Destinations: []string{ + "GroupSwarm", + }, + Sources: []string{ + "GroupAll", + }, + Bidirectional: false, + Protocol: PolicyRuleProtocolTCP, + Ports: []string{"80"}, + }, + }, + SourcePostureChecks: []string{ + "PostureChecksDefault", + }, + }) + + t.Run("verify peer's network map with default group peer list", func(t *testing.T) { + // peerB doesn't fulfill the NB posture check but is included in the destination group Swarm, + // will establish a connection with all source peers satisfying the NB posture check. + peers, firewallRules := account.getPeerConnectionResources("peerB") + assert.Len(t, peers, 4) + assert.Len(t, firewallRules, 4) + assert.Contains(t, peers, account.Peers["peerA"]) + assert.Contains(t, peers, account.Peers["peerC"]) + assert.Contains(t, peers, account.Peers["peerD"]) + assert.Contains(t, peers, account.Peers["peerF"]) + + // peerC satisfy the NB posture check, should establish connection to all destination group peer's + // We expect a single permissive firewall rule which all outgoing connections + peers, firewallRules = account.getPeerConnectionResources("peerC") + assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) + assert.Len(t, firewallRules, 1) + expectedFirewallRules := []*FirewallRule{ + { + PeerIP: "0.0.0.0", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + } + assert.ElementsMatch(t, firewallRules, expectedFirewallRules) + + // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, + // all source group peers satisfying the NB posture check should establish connection + peers, firewallRules = account.getPeerConnectionResources("peerE") + assert.Len(t, peers, 4) + assert.Len(t, firewallRules, 4) + assert.Contains(t, peers, account.Peers["peerA"]) + assert.Contains(t, peers, account.Peers["peerC"]) + assert.Contains(t, peers, account.Peers["peerD"]) + assert.Contains(t, peers, account.Peers["peerF"]) + }) + + t.Run("verify peer's network map with modified group peer list", func(t *testing.T) { + // Removing peerB as the part of destination group Swarm + account.Groups["GroupSwarm"].Peers = []string{"peerA", "peerD", "peerE", "peerG", "peerH"} + + // peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's + // no connection should be established to any peer of destination group + peers, firewallRules := account.getPeerConnectionResources("peerB") + assert.Len(t, peers, 0) + assert.Len(t, firewallRules, 0) + + // peerC satisfy the NB posture check, should establish connection to all destination group peer's + // We expect a single permissive firewall rule which all outgoing connections + peers, firewallRules = account.getPeerConnectionResources("peerC") + assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) + assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers)) + + peerIDs := make([]string, 0, len(peers)) + for _, peer := range peers { + peerIDs = append(peerIDs, peer.ID) + } + assert.ElementsMatch(t, peerIDs, account.Groups["GroupSwarm"].Peers) + + // Removing peerF as the part of source group All + account.Groups["GroupAll"].Peers = []string{"peerB", "peerA", "peerD", "peerC", "peerG", "peerH"} + + // peerE doesn't fulfill the NB posture check and exists in only destination group Swarm, + // all source group peers satisfying the NB posture check should establish connection + peers, firewallRules = account.getPeerConnectionResources("peerE") + assert.Len(t, peers, 3) + assert.Len(t, firewallRules, 3) + assert.Contains(t, peers, account.Peers["peerA"]) + assert.Contains(t, peers, account.Peers["peerC"]) + assert.Contains(t, peers, account.Peers["peerD"]) + + peers, firewallRules = account.getPeerConnectionResources("peerA") + assert.Len(t, peers, 5) + // assert peers from Group Swarm + assert.Contains(t, peers, account.Peers["peerD"]) + assert.Contains(t, peers, account.Peers["peerE"]) + assert.Contains(t, peers, account.Peers["peerG"]) + assert.Contains(t, peers, account.Peers["peerH"]) + + // assert peers from Group All + assert.Contains(t, peers, account.Peers["peerC"]) + + expectedFirewallRules := []*FirewallRule{ + { + PeerIP: "100.65.62.5", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + { + PeerIP: "100.65.32.206", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + { + PeerIP: "100.65.13.186", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + { + PeerIP: "100.65.29.55", + Direction: firewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + { + PeerIP: "100.65.254.139", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + { + PeerIP: "100.65.62.5", + Direction: firewallRuleDirectionIN, + Action: "accept", + Protocol: "tcp", + Port: "80", + }, + } + assert.Len(t, firewallRules, len(expectedFirewallRules)) + assert.ElementsMatch(t, firewallRules, expectedFirewallRules) + }) +} + func sortFunc() func(a *FirewallRule, b *FirewallRule) int { return func(a, b *FirewallRule) int { // Concatenate PeerIP and Direction as string for comparison diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index a585836d16b..f9722594466 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -12,7 +12,7 @@ const ( // Check represents an interface for performing a check on a peer. type Check interface { - Check(peer nbpeer.Peer) error + Check(peer nbpeer.Peer) (bool, error) Name() string } diff --git a/management/server/posture/version.go b/management/server/posture/version.go index 46672a967df..1fe0aa57389 100644 --- a/management/server/posture/version.go +++ b/management/server/posture/version.go @@ -1,9 +1,8 @@ package posture import ( - "fmt" - "github.com/hashicorp/go-version" + log "github.com/sirupsen/logrus" nbpeer "github.com/netbirdio/netbird/management/server/peer" ) @@ -14,25 +13,27 @@ type NBVersionCheck struct { var _ Check = (*NBVersionCheck)(nil) -func (n *NBVersionCheck) Check(peer nbpeer.Peer) error { +func (n *NBVersionCheck) Check(peer nbpeer.Peer) (bool, error) { peerNBVersion, err := version.NewVersion(peer.Meta.WtVersion) if err != nil { - return err + return false, err } constraints, err := version.NewConstraint(">= " + n.MinVersion) if err != nil { - return err + return false, err } if constraints.Check(peerNBVersion) { - return nil + return true, nil } - return fmt.Errorf("peer NB version %s is older than minimum allowed version %s", + log.Debugf("peer %s NB version %s is older than minimum allowed version %s", + peer.ID, peer.Meta.UIVersion, n.MinVersion, ) + return false, nil } func (n *NBVersionCheck) Name() string { diff --git a/management/server/posture/version_test.go b/management/server/posture/version_test.go index c590fd2aeb3..de51c2283b1 100644 --- a/management/server/posture/version_test.go +++ b/management/server/posture/version_test.go @@ -14,6 +14,7 @@ func TestNBVersionCheck_Check(t *testing.T) { input peer.Peer check NBVersionCheck wantErr bool + isValid bool }{ { name: "Valid Peer NB version", @@ -26,6 +27,7 @@ func TestNBVersionCheck_Check(t *testing.T) { MinVersion: "1.0.0", }, wantErr: false, + isValid: true, }, { name: "Valid Peer NB version With No Patch Version 1", @@ -38,6 +40,7 @@ func TestNBVersionCheck_Check(t *testing.T) { MinVersion: "2.0", }, wantErr: false, + isValid: true, }, { name: "Valid Peer NB version With No Patch Version 2", @@ -50,6 +53,7 @@ func TestNBVersionCheck_Check(t *testing.T) { MinVersion: "2.0", }, wantErr: false, + isValid: true, }, { name: "Older Peer NB version", @@ -61,7 +65,8 @@ func TestNBVersionCheck_Check(t *testing.T) { check: NBVersionCheck{ MinVersion: "1.0.0", }, - wantErr: true, + wantErr: false, + isValid: false, }, { name: "Older Peer NB version With Patch Version", @@ -73,7 +78,8 @@ func TestNBVersionCheck_Check(t *testing.T) { check: NBVersionCheck{ MinVersion: "0.2", }, - wantErr: true, + wantErr: false, + isValid: false, }, { name: "Invalid Peer NB version", @@ -86,17 +92,19 @@ func TestNBVersionCheck_Check(t *testing.T) { MinVersion: "1.0.0", }, wantErr: true, + isValid: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.check.Check(tt.input) + isValid, err := tt.check.Check(tt.input) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } + assert.Equal(t, tt.isValid, isValid) }) } } From 786326aba9fe68b21bbd1908ad4a2cf83c1df045 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 26 Jan 2024 15:50:56 +0300 Subject: [PATCH 12/35] Extend NetBird agent to collect kernel version (#1495) * Add KernelVersion field to LoginRequest * Add KernelVersion to system info retrieval * Fix tests * Remove Core field from system info * Replace Core field with new OSVersion field in system info * Added WMI dependency to info_windows.go --- client/system/info.go | 2 +- client/system/info_android.go | 7 +- client/system/info_darwin.go | 2 +- client/system/info_freebsd.go | 2 +- client/system/info_ios.go | 2 +- client/system/info_linux.go | 2 +- client/system/info_windows.go | 2 +- management/client/client_test.go | 3 +- management/client/grpc.go | 3 +- management/proto/management.pb.go | 548 +++++++++++++----------- management/proto/management.proto | 5 +- management/server/grpcserver.go | 17 +- management/server/http/peers_handler.go | 12 +- management/server/peer/peer.go | 20 +- 14 files changed, 337 insertions(+), 290 deletions(-) diff --git a/client/system/info.go b/client/system/info.go index 2d5b7192ece..7a5ae9c609e 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -23,7 +23,6 @@ const OsNameCtxKey = "OsName" type Info struct { GoOS string Kernel string - Core string Platform string OS string OSVersion string @@ -31,6 +30,7 @@ type Info struct { CPUs int WiretrusteeVersion string UIVersion string + KernelVersion string } // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context diff --git a/client/system/info_android.go b/client/system/info_android.go index 9a1a7befb30..be62352f709 100644 --- a/client/system/info_android.go +++ b/client/system/info_android.go @@ -23,7 +23,12 @@ func GetInfo(ctx context.Context) *Info { kernel = osInfo[1] } - gio := &Info{Kernel: kernel, Core: osVersion(), Platform: "unknown", OS: "android", OSVersion: osVersion(), GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + var kernelVersion string + if len(osInfo) > 2 { + kernelVersion = osInfo[2] + } + + gio := &Info{Kernel: kernel, Platform: "unknown", OS: "android", OSVersion: osVersion(), GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: kernelVersion} gio.Hostname = extractDeviceName(ctx, "android") gio.WiretrusteeVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 5ae2b4fc676..b35b3a3af85 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -33,7 +33,7 @@ func GetInfo(ctx context.Context) *Info { log.Warnf("got an error while retrieving macOS version with sw_vers, error: %s. Using darwin version instead.\n", err) swVersion = []byte(release) } - gio := &Info{Kernel: sysName, OSVersion: strings.TrimSpace(string(swVersion)), Core: release, Platform: machine, OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + gio := &Info{Kernel: sysName, OSVersion: strings.TrimSpace(string(swVersion)), Platform: machine, OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: release} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() diff --git a/client/system/info_freebsd.go b/client/system/info_freebsd.go index 6c2d8a70165..74b132f4a98 100644 --- a/client/system/info_freebsd.go +++ b/client/system/info_freebsd.go @@ -23,7 +23,7 @@ func GetInfo(ctx context.Context) *Info { osStr := strings.Replace(out, "\n", "", -1) osStr = strings.Replace(osStr, "\r\n", "", -1) osInfo := strings.Split(osStr, " ") - gio := &Info{Kernel: osInfo[0], Core: osInfo[1], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + gio := &Info{Kernel: osInfo[0], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1]} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() diff --git a/client/system/info_ios.go b/client/system/info_ios.go index c0e51ec6001..e1c291ef591 100644 --- a/client/system/info_ios.go +++ b/client/system/info_ios.go @@ -17,7 +17,7 @@ func GetInfo(ctx context.Context) *Info { sysName := extractOsName(ctx, "sysName") swVersion := extractOsVersion(ctx, "swVersion") - gio := &Info{Kernel: sysName, OSVersion: swVersion, Core: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + gio := &Info{Kernel: sysName, OSVersion: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: swVersion} gio.Hostname = extractDeviceName(ctx, "hostname") gio.WiretrusteeVersion = version.NetbirdVersion() gio.UIVersion = extractUserAgent(ctx) diff --git a/client/system/info_linux.go b/client/system/info_linux.go index 21a4d482a64..e2b60b0562e 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -50,7 +50,7 @@ func GetInfo(ctx context.Context) *Info { if osName == "" { osName = osInfo[3] } - gio := &Info{Kernel: osInfo[0], Core: osInfo[1], Platform: osInfo[2], OS: osName, OSVersion: osVer, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + gio := &Info{Kernel: osInfo[0], Platform: osInfo[2], OS: osName, OSVersion: osVer, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: osInfo[1]} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() diff --git a/client/system/info_windows.go b/client/system/info_windows.go index 76b13bbc304..d343063ffaf 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -22,7 +22,7 @@ type Win32_OperatingSystem struct { func GetInfo(ctx context.Context) *Info { osName, osVersion := getOSNameAndVersion() buildVersion := getBuildVersion() - gio := &Info{Kernel: "windows", OSVersion: osVersion, Core: buildVersion, Platform: "unknown", OS: osName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()} + gio := &Info{Kernel: "windows", OSVersion: osVersion, Platform: "unknown", OS: osName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: buildVersion} systemHostname, _ := os.Hostname() gio.Hostname = extractDeviceName(ctx, systemHostname) gio.WiretrusteeVersion = version.NetbirdVersion() diff --git a/management/client/client_test.go b/management/client/client_test.go index 9ebb58420c1..cd15bf7be11 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -348,10 +348,11 @@ func Test_SystemMetaDataFromClient(t *testing.T) { Hostname: info.Hostname, GoOS: info.GoOS, Kernel: info.Kernel, - Core: info.OSVersion, Platform: info.Platform, OS: info.OS, + OSVersion: info.OSVersion, WiretrusteeVersion: info.WiretrusteeVersion, + KernelVersion: info.KernelVersion, } assert.Equal(t, ValidKey, actualValidKey) diff --git a/management/client/grpc.go b/management/client/grpc.go index ddb420ee20e..dfb09bbf793 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -428,10 +428,11 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { Hostname: info.Hostname, GoOS: info.GoOS, OS: info.OS, - Core: info.OSVersion, + OSVersion: info.OSVersion, Platform: info.Platform, Kernel: info.Kernel, WiretrusteeVersion: info.WiretrusteeVersion, UiVersion: info.UIVersion, + KernelVersion: info.KernelVersion, } } diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 45ef49e1f7e..60e41e86883 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.9 +// protoc v4.23.4 // source: management.proto package proto @@ -595,14 +595,19 @@ type PeerSystemMeta struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` - GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` - Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` + Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` + // core field has been deprecated in favor of the new OsVersion field for better clarity. + // + // Deprecated: Do not use. Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` WiretrusteeVersion string `protobuf:"bytes,7,opt,name=wiretrusteeVersion,proto3" json:"wiretrusteeVersion,omitempty"` UiVersion string `protobuf:"bytes,8,opt,name=uiVersion,proto3" json:"uiVersion,omitempty"` + KernelVersion string `protobuf:"bytes,9,opt,name=kernelVersion,proto3" json:"kernelVersion,omitempty"` + OSVersion string `protobuf:"bytes,10,opt,name=OSVersion,proto3" json:"OSVersion,omitempty"` } func (x *PeerSystemMeta) Reset() { @@ -658,6 +663,7 @@ func (x *PeerSystemMeta) GetKernel() string { return "" } +// Deprecated: Do not use. func (x *PeerSystemMeta) GetCore() string { if x != nil { return x.Core @@ -693,6 +699,20 @@ func (x *PeerSystemMeta) GetUiVersion() string { return "" } +func (x *PeerSystemMeta) GetKernelVersion() string { + if x != nil { + return x.KernelVersion + } + return "" +} + +func (x *PeerSystemMeta) GetOSVersion() string { + if x != nil { + return x.OSVersion + } + return "" +} + type LoginResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2257,271 +2277,275 @@ var file_management_proto_rawDesc = []byte{ 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0xe6, 0x01, 0x0a, 0x0e, + 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0xae, 0x02, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, - 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, - 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, - 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, - 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x79, 0x0a, 0x11, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, - 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, - 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, - 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, - 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, - 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, - 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, - 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, - 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, - 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0xe2, 0x03, 0x0a, 0x0a, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, - 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, - 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, - 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, - 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, - 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, - 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, - 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, - 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, - 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, - 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, - 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, - 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, - 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, - 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, - 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, - 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, - 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, - 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, - 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, - 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, - 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, - 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, - 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, - 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, - 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, - 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, - 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, - 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, - 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, - 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, - 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, - 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, - 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, - 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, - 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, - 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, - 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, - 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, - 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, - 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, - 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, - 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, - 0x10, 0x04, 0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, - 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, + 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, + 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, + 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, + 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, + 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, + 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, + 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, + 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, + 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, + 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, + 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, + 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, + 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, + 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, + 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, + 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, + 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, + 0x61, 0x6c, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, + 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, + 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x7d, 0x0a, + 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x01, 0x0a, + 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, + 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x22, 0xe2, 0x03, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, + 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, + 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, + 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, + 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, + 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, + 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, + 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, + 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, + 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, + 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, + 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, + 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, + 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, + 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, + 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, + 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, + 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, + 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, + 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, + 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, + 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, + 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, + 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, + 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, + 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, + 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, + 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, + 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, + 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, + 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, + 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, + 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, + 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, + 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, + 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, + 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, + 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, + 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, + 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, + 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, + 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, + 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, + 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, + 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, + 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, + 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, + 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, + 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, + 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, + 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, + 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, - 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, - 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, - 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, - 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, + 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, + 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, + 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, + 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, + 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, + 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index ae90beaf3d0..a49235dfcdf 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -97,11 +97,14 @@ message PeerSystemMeta { string hostname = 1; string goOS = 2; string kernel = 3; - string core = 4; + // core field has been deprecated in favor of the new OsVersion field for better clarity. + string core = 4 [deprecated=true]; string platform = 5; string OS = 6; string wiretrusteeVersion = 7; string uiVersion = 8; + string kernelVersion = 9; + string OSVersion = 10; } message LoginResponse { diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index d6463edd9b6..38880198851 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -250,14 +250,15 @@ func mapError(err error) error { func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { return nbpeer.PeerSystemMeta{ - Hostname: loginReq.GetMeta().GetHostname(), - GoOS: loginReq.GetMeta().GetGoOS(), - Kernel: loginReq.GetMeta().GetKernel(), - Core: loginReq.GetMeta().GetCore(), - Platform: loginReq.GetMeta().GetPlatform(), - OS: loginReq.GetMeta().GetOS(), - WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(), - UIVersion: loginReq.GetMeta().GetUiVersion(), + Hostname: loginReq.GetMeta().GetHostname(), + GoOS: loginReq.GetMeta().GetGoOS(), + Kernel: loginReq.GetMeta().GetKernel(), + Platform: loginReq.GetMeta().GetPlatform(), + OS: loginReq.GetMeta().GetOS(), + OSVersion: loginReq.GetMeta().GetOSVersion(), + WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(), + UIVersion: loginReq.GetMeta().GetUiVersion(), + KernelVersion: loginReq.GetMeta().GetKernelVersion(), } } diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index 734785e308a..2878136df63 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -231,13 +231,17 @@ func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMin } func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer { + osVersion := peer.Meta.OSVersion + if osVersion == "" { + osVersion = peer.Meta.Core + } return &api.Peer{ Id: peer.ID, Name: peer.Name, Ip: peer.IP.String(), Connected: peer.Status.Connected, LastSeen: peer.Status.LastSeen, - Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), + Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Version: peer.Meta.WtVersion, Groups: groupsInfo, SshEnabled: peer.SSHEnabled, @@ -254,13 +258,17 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD } func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeersCount int) *api.PeerBatch { + osVersion := peer.Meta.OSVersion + if osVersion == "" { + osVersion = peer.Meta.Core + } return &api.PeerBatch{ Id: peer.ID, Name: peer.Name, Ip: peer.IP.String(), Connected: peer.Status.Connected, LastSeen: peer.Status.LastSeen, - Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), + Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), Version: peer.Meta.WtVersion, Groups: groupsInfo, SshEnabled: peer.SSHEnabled, diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index a4e4cc3aa36..56462a4a334 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -56,23 +56,27 @@ type PeerStatus struct { // PeerSystemMeta is a metadata of a Peer machine system type PeerSystemMeta struct { - Hostname string - GoOS string - Kernel string - Core string - Platform string - OS string - WtVersion string - UIVersion string + Hostname string + GoOS string + Kernel string + Core string + Platform string + OS string + OSVersion string + WtVersion string + UIVersion string + KernelVersion string } func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { return p.Hostname == other.Hostname && p.GoOS == other.GoOS && p.Kernel == other.Kernel && + p.KernelVersion == other.KernelVersion && p.Core == other.Core && p.Platform == other.Platform && p.OS == other.OS && + p.OSVersion == other.OSVersion && p.WtVersion == other.WtVersion && p.UIVersion == other.UIVersion } From ad42ead5eb34e72665eea088c5df1b21fc7e22fc Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 29 Jan 2024 13:51:38 +0100 Subject: [PATCH 13/35] Add OS Version posture checks (#1479) --- management/server/http/api/openapi.yml | 53 ++++- management/server/http/api/types.gen.go | 38 +++- .../server/http/posture_checks_handler.go | 35 ++- .../http/posture_checks_handler_test.go | 205 ++++++++++++++++-- management/server/policy_test.go | 67 +++++- management/server/posture/checks.go | 8 +- .../posture/{version.go => nb_version.go} | 6 +- .../{version_test.go => nb_version_test.go} | 0 management/server/posture/os_version.go | 96 ++++++++ management/server/posture/os_version_test.go | 120 ++++++++++ 10 files changed, 574 insertions(+), 54 deletions(-) rename management/server/posture/{version.go => nb_version.go} (93%) rename management/server/posture/{version_test.go => nb_version_test.go} (100%) create mode 100644 management/server/posture/os_version.go create mode 100644 management/server/posture/os_version_test.go diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index e74e06919e0..e7166b0aa73 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -21,6 +21,8 @@ tags: description: Interact with and view information about rules. - name: Policies description: Interact with and view information about policies. + - name: Posture Checks + description: Interact with and view information about posture checks. - name: Routes description: Interact with and view information about routes. - name: DNS @@ -838,16 +840,50 @@ components: properties: nb_version_check: $ref: '#/components/schemas/NBVersionCheck' + os_version_check: + $ref: '#/components/schemas/OSVersionCheck' NBVersionCheck: description: Posture check for the version of NetBird type: object + $ref: '#/components/schemas/MinVersionCheck' + OSVersionCheck: + description: Posture check for the version of operating system + type: object + properties: + android: + description: Minimum version of Android + $ref: '#/components/schemas/MinVersionCheck' + darwin: + $ref: '#/components/schemas/MinVersionCheck' + ios: + description: Minimum version of iOS + $ref: '#/components/schemas/MinVersionCheck' + linux: + description: Minimum version of Linux + $ref: '#/components/schemas/MinKernelVersionCheck' + windows: + description: Minimum version of Windows + $ref: '#/components/schemas/MinKernelVersionCheck' + MinVersionCheck: + description: Posture check for the version of operating system + type: object properties: min_version: - description: Minimum acceptable NetBird version + description: Minimum acceptable version type: string - example: "0.25.0" + example: "14.3" required: - min_version + MinKernelVersionCheck: + description: Posture check for the version of kernel + type: object + properties: + min_kernel_version: + description: Minimum acceptable version + type: string + example: "6.6.12" + required: + - min_kernel_version PostureCheckUpdate: type: object properties: @@ -2215,7 +2251,6 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" - /api/routes/{routeId}: get: summary: Retrieve a Route @@ -2360,7 +2395,6 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" - /api/dns/nameservers/{nsgroupId}: get: summary: Retrieve a Nameserver Group @@ -2452,7 +2486,6 @@ paths: "$ref": "#/components/responses/forbidden" '500': "$ref": "#/components/responses/internal_error" - /api/dns/settings: get: summary: Retrieve DNS settings @@ -2534,7 +2567,7 @@ paths: get: summary: List all Posture Checks description: Returns a list of all posture checks - tags: [ Posture Checks ] + tags: [ "Posture Checks" ] security: - BearerAuth: [ ] - TokenAuth: [ ] @@ -2558,7 +2591,7 @@ paths: post: summary: Create a Posture Check description: Creates a posture check - tags: [ Posture Checks ] + tags: [ "Posture Checks" ] security: - BearerAuth: [ ] - TokenAuth: [ ] @@ -2579,7 +2612,7 @@ paths: get: summary: Retrieve a Posture Check description: Get information about a posture check - tags: [ Posture Checks ] + tags: [ "Posture Checks" ] security: - BearerAuth: [ ] - TokenAuth: [ ] @@ -2608,7 +2641,7 @@ paths: put: summary: Update a Posture Check description: Update/Replace a posture check - tags: [ Posture Checks ] + tags: [ "Posture Checks" ] security: - BearerAuth: [ ] - TokenAuth: [ ] @@ -2643,7 +2676,7 @@ paths: delete: summary: Delete a Posture Check description: Delete a posture check - tags: [ Posture Checks ] + tags: [ "Posture Checks" ] security: - BearerAuth: [ ] - TokenAuth: [ ] diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index b6291c9f245..aed6adaf9cd 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -178,8 +178,11 @@ type AccountSettings struct { // Checks List of objects that perform the actual checks type Checks struct { - // NbVersionCheck Posture check for the version of NetBird + // NbVersionCheck Posture check for the version of operating system NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` + + // OsVersionCheck Posture check for the version of operating system + OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"` } // DNSSettings defines model for DNSSettings. @@ -263,12 +266,21 @@ type GroupRequest struct { Peers *[]string `json:"peers,omitempty"` } -// NBVersionCheck Posture check for the version of NetBird -type NBVersionCheck struct { - // MinVersion Minimum acceptable NetBird version +// MinKernelVersionCheck Posture check for the version of kernel +type MinKernelVersionCheck struct { + // MinKernelVersion Minimum acceptable version + MinKernelVersion string `json:"min_kernel_version"` +} + +// MinVersionCheck Posture check for the version of operating system +type MinVersionCheck struct { + // MinVersion Minimum acceptable version MinVersion string `json:"min_version"` } +// NBVersionCheck Posture check for the version of operating system +type NBVersionCheck = MinVersionCheck + // Nameserver defines model for Nameserver. type Nameserver struct { // Ip Nameserver IP @@ -341,6 +353,24 @@ type NameserverGroupRequest struct { SearchDomainsEnabled bool `json:"search_domains_enabled"` } +// OSVersionCheck Posture check for the version of operating system +type OSVersionCheck struct { + // Android Posture check for the version of operating system + Android *MinVersionCheck `json:"android,omitempty"` + + // Darwin Posture check for the version of operating system + Darwin *MinVersionCheck `json:"darwin,omitempty"` + + // Ios Posture check for the version of operating system + Ios *MinVersionCheck `json:"ios,omitempty"` + + // Linux Posture check for the version of kernel + Linux *MinKernelVersionCheck `json:"linux,omitempty"` + + // Windows Posture check for the version of kernel + Windows *MinKernelVersionCheck `json:"windows,omitempty"` +} + // Peer defines model for Peer. type Peer struct { // AccessiblePeers List of accessible peers diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index e40f4a751ca..123b7e5bc91 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -183,7 +183,16 @@ func (p *PostureChecksHandler) savePostureChecks( postureChecks.Checks = append(postureChecks.Checks, &posture.NBVersionCheck{ MinVersion: nbVersionCheck.MinVersion, }) + } + if osVersionCheck := req.Checks.OsVersionCheck; osVersionCheck != nil { + postureChecks.Checks = append(postureChecks.Checks, &posture.OSVersionCheck{ + Android: (*posture.MinVersionCheck)(osVersionCheck.Android), + Darwin: (*posture.MinVersionCheck)(osVersionCheck.Darwin), + Ios: (*posture.MinVersionCheck)(osVersionCheck.Ios), + Linux: (*posture.MinKernelVersionCheck)(osVersionCheck.Linux), + Windows: (*posture.MinKernelVersionCheck)(osVersionCheck.Windows), + }) } if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { @@ -199,7 +208,7 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { return status.Errorf(status.InvalidArgument, "posture checks name shouldn't be empty") } - if req.Checks == nil || req.Checks.NbVersionCheck == nil { + if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil) { return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") } @@ -207,19 +216,41 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { return status.Errorf(status.InvalidArgument, "minimum version for NetBird's version check shouldn't be empty") } + if osVersionCheck := req.Checks.OsVersionCheck; osVersionCheck != nil { + emptyOS := osVersionCheck.Android == nil && osVersionCheck.Darwin == nil && osVersionCheck.Ios == nil && + osVersionCheck.Linux == nil && osVersionCheck.Windows == nil + emptyMinVersion := osVersionCheck.Android != nil && osVersionCheck.Android.MinVersion == "" || + osVersionCheck.Darwin != nil && osVersionCheck.Darwin.MinVersion == "" || + osVersionCheck.Ios != nil && osVersionCheck.Ios.MinVersion == "" || + osVersionCheck.Linux != nil && osVersionCheck.Linux.MinKernelVersion == "" || + osVersionCheck.Windows != nil && osVersionCheck.Windows.MinKernelVersion == "" + if emptyOS || emptyMinVersion { + return status.Errorf(status.InvalidArgument, + "minimum version for at least one OS in the OS version check shouldn't be empty") + } + } + return nil } func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { var checks api.Checks for _, check := range postureChecks.Checks { - //nolint:gocritic switch check.Name() { case posture.NBVersionCheckName: versionCheck := check.(*posture.NBVersionCheck) checks.NbVersionCheck = &api.NBVersionCheck{ MinVersion: versionCheck.MinVersion, } + case posture.OSVersionCheckName: + osCheck := check.(*posture.OSVersionCheck) + checks.OsVersionCheck = &api.OSVersionCheck{ + Android: (*api.MinVersionCheck)(osCheck.Android), + Darwin: (*api.MinVersionCheck)(osCheck.Darwin), + Ios: (*api.MinVersionCheck)(osCheck.Ios), + Linux: (*api.MinKernelVersionCheck)(osCheck.Linux), + Windows: (*api.MinKernelVersionCheck)(osCheck.Windows), + } } } diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index a89d14565d7..acba6ef5ca1 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -80,45 +80,67 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksH } func TestGetPostureCheck(t *testing.T) { + postureCheck := &posture.Checks{ + ID: "postureCheck", + Name: "nbVersion", + Checks: []posture.Check{ + &posture.NBVersionCheck{ + MinVersion: "1.0.0", + }, + }, + } + osPostureCheck := &posture.Checks{ + ID: "osPostureCheck", + Name: "osVersion", + Checks: []posture.Check{ + &posture.OSVersionCheck{ + Linux: &posture.MinKernelVersionCheck{ + MinKernelVersion: "6.0.0", + }, + Darwin: &posture.MinVersionCheck{ + MinVersion: "14", + }, + Ios: &posture.MinVersionCheck{ + MinVersion: "", + }, + }, + }, + } tt := []struct { name string + id string + checkName string expectedStatus int expectedBody bool - requestType string - requestPath string requestBody io.Reader }{ { - name: "GetPostureCheck OK", + name: "GetPostureCheck NBVersion OK", expectedBody: true, - requestType: http.MethodGet, - requestPath: "/api/posture-checks/postureCheck", + id: postureCheck.ID, + checkName: postureCheck.Name, + expectedStatus: http.StatusOK, + }, + { + name: "GetPostureCheck OSVersion OK", + expectedBody: true, + id: osPostureCheck.ID, + checkName: osPostureCheck.Name, expectedStatus: http.StatusOK, }, { name: "GetPostureCheck Not Found", - requestType: http.MethodGet, - requestPath: "/api/posture-checks/not-exists", + id: "not-exists", expectedStatus: http.StatusNotFound, }, } - postureCheck := &posture.Checks{ - ID: "postureCheck", - Name: "name", - Checks: []posture.Check{ - &posture.NBVersionCheck{ - MinVersion: "1.0.0", - }, - }, - } - - p := initPostureChecksTestData(postureCheck) + p := initPostureChecksTestData(postureCheck, osPostureCheck) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { recorder := httptest.NewRecorder() - req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + req := httptest.NewRequest(http.MethodGet, "/api/posture-checks/"+tc.id, tc.requestBody) router := mux.NewRouter() router.HandleFunc("/api/posture-checks/{postureCheckId}", p.GetPostureCheck).Methods("GET") @@ -147,8 +169,8 @@ func TestGetPostureCheck(t *testing.T) { t.Fatalf("Sent content is not in correct json format; %v", err) } - assert.Equal(t, got.Id, postureCheck.ID) - assert.Equal(t, got.Name, postureCheck.Name) + assert.Equal(t, got.Id, tc.id) + assert.Equal(t, got.Name, tc.checkName) }) } } @@ -165,7 +187,7 @@ func TestPostureCheckUpdate(t *testing.T) { requestBody io.Reader }{ { - name: "Create Posture Checks", + name: "Create Posture Checks NB version", requestType: http.MethodPost, requestPath: "/api/posture-checks", requestBody: bytes.NewBuffer( @@ -191,6 +213,49 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Create Posture Checks OS version", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "os_version_check": { + "android": { + "min_version": "9.0.0" + }, + "ios": { + "min_version": "17.0" + }, + "linux": { + "min_kernel_version": "6.0.0" + } + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Android: &api.MinVersionCheck{ + MinVersion: "9.0.0", + }, + Ios: &api.MinVersionCheck{ + MinVersion: "17.0", + }, + Linux: &api.MinKernelVersionCheck{ + MinKernelVersion: "6.0.0", + }, + }, + }, + }, + }, { name: "Create Posture Checks Invalid Check", requestType: http.MethodPost, @@ -237,7 +302,7 @@ func TestPostureCheckUpdate(t *testing.T) { expectedBody: false, }, { - name: "Update Posture Checks", + name: "Update Posture Checks NB Version", requestType: http.MethodPut, requestPath: "/api/posture-checks/postureCheck", requestBody: bytes.NewBuffer( @@ -262,6 +327,36 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Update Posture Checks OS Version", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/osPostureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "os_version_check": { + "linux": { + "min_kernel_version": "6.9.0" + } + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str(""), + Checks: api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Linux: &api.MinKernelVersionCheck{ + MinKernelVersion: "6.9.0", + }, + }, + }, + }, + }, { name: "Update Posture Checks Invalid Check", requestType: http.MethodPut, @@ -317,7 +412,19 @@ func TestPostureCheckUpdate(t *testing.T) { MinVersion: "1.0.0", }, }, - }) + }, + &posture.Checks{ + ID: "osPostureCheck", + Name: "osPostureCheck", + Checks: []posture.Check{ + &posture.OSVersionCheck{ + Linux: &posture.MinKernelVersionCheck{ + MinKernelVersion: "5.0.0", + }, + }, + }, + }, + ) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { @@ -358,3 +465,53 @@ func TestPostureCheckUpdate(t *testing.T) { }) } } + +func TestPostureCheck_validatePostureChecksUpdate(t *testing.T) { + // empty name + err := validatePostureChecksUpdate(api.PostureCheckUpdate{}) + assert.Error(t, err) + + // empty checks + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default"}) + assert.Error(t, err) + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{}}) + assert.Error(t, err) + + // not valid NbVersionCheck + nbVersionCheck := api.NBVersionCheck{} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{NbVersionCheck: &nbVersionCheck}}) + assert.Error(t, err) + + // valid NbVersionCheck + nbVersionCheck = api.NBVersionCheck{MinVersion: "1.0"} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{NbVersionCheck: &nbVersionCheck}}) + assert.NoError(t, err) + + // not valid OsVersionCheck + osVersionCheck := api.OSVersionCheck{} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) + assert.Error(t, err) + + // not valid OsVersionCheck + osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{}} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) + assert.Error(t, err) + + // not valid OsVersionCheck + osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{}, Darwin: &api.MinVersionCheck{MinVersion: "14.2"}} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) + assert.Error(t, err) + + // valid OsVersionCheck + osVersionCheck = api.OSVersionCheck{Linux: &api.MinKernelVersionCheck{MinKernelVersion: "6.0"}} + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) + assert.NoError(t, err) + + // valid OsVersionCheck + osVersionCheck = api.OSVersionCheck{ + Linux: &api.MinKernelVersionCheck{MinKernelVersion: "6.0"}, + Darwin: &api.MinVersionCheck{MinVersion: "14.2"}, + } + err = validatePostureChecksUpdate(api.PostureCheckUpdate{Name: "Default", Checks: &api.Checks{OsVersionCheck: &osVersionCheck}}) + assert.NoError(t, err) +} diff --git a/management/server/policy_test.go b/management/server/policy_test.go index d80841bde96..57c33962e94 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -452,7 +452,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.14.88"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.25.9", + GoOS: "linux", + KernelVersion: "6.6.7", + WtVersion: "0.25.9", }, }, "peerB": { @@ -460,7 +462,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.80.39"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.23.0", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.23.0", }, }, "peerC": { @@ -468,7 +472,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.254.139"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.25.8", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.25.8", }, }, "peerD": { @@ -476,7 +482,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.62.5"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.25.9", + GoOS: "linux", + KernelVersion: "6.6.0", + WtVersion: "0.25.9", }, }, "peerE": { @@ -484,7 +492,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.32.206"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.24.0", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.24.0", }, }, "peerF": { @@ -492,7 +502,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.250.202"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.25.9", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.25.9", }, }, "peerG": { @@ -500,7 +512,9 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.13.186"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.23.2", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.23.2", }, }, "peerH": { @@ -508,7 +522,19 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { IP: net.ParseIP("100.65.29.55"), Status: &nbpeer.PeerStatus{}, Meta: nbpeer.PeerSystemMeta{ - WtVersion: "0.23.1", + GoOS: "linux", + KernelVersion: "6.6.1", + WtVersion: "0.23.1", + }, + }, + "peerI": { + ID: "peerI", + IP: net.ParseIP("100.65.21.56"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{ + GoOS: "windows", + KernelVersion: "10.0.14393.2430", + WtVersion: "0.25.1", }, }, }, @@ -524,6 +550,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { "peerF", "peerG", "peerH", + "peerI", }, }, "GroupSwarm": { @@ -536,6 +563,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { "peerE", "peerG", "peerH", + "peerI", }, }, }, @@ -543,11 +571,16 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { { ID: "PostureChecksDefault", Name: "Default", - Description: "This is a posture checks that check if peer is running required version", + Description: "This is a posture checks that check if peer is running required versions", Checks: []posture.Check{ &posture.NBVersionCheck{ MinVersion: "0.25", }, + &posture.OSVersionCheck{ + Linux: &posture.MinKernelVersionCheck{ + MinKernelVersion: "6.6.0", + }, + }, }, }, }, @@ -616,6 +649,16 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { assert.Contains(t, peers, account.Peers["peerC"]) assert.Contains(t, peers, account.Peers["peerD"]) assert.Contains(t, peers, account.Peers["peerF"]) + + // peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm, + // all source group peers satisfying the NB posture check should establish connection + peers, firewallRules = account.getPeerConnectionResources("peerI") + assert.Len(t, peers, 4) + assert.Len(t, firewallRules, 4) + assert.Contains(t, peers, account.Peers["peerA"]) + assert.Contains(t, peers, account.Peers["peerC"]) + assert.Contains(t, peers, account.Peers["peerD"]) + assert.Contains(t, peers, account.Peers["peerF"]) }) t.Run("verify peer's network map with modified group peer list", func(t *testing.T) { @@ -628,6 +671,12 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { assert.Len(t, peers, 0) assert.Len(t, firewallRules, 0) + // peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's + // no connection should be established to any peer of destination group + peers, firewallRules = account.getPeerConnectionResources("peerI") + assert.Len(t, peers, 0) + assert.Len(t, firewallRules, 0) + // peerC satisfy the NB posture check, should establish connection to all destination group peer's // We expect a single permissive firewall rule which all outgoing connections peers, firewallRules = account.getPeerConnectionResources("peerC") diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index f9722594466..521546851ed 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -8,6 +8,7 @@ import ( const ( NBVersionCheckName = "NBVersionCheck" + OSVersionCheckName = "OSVersionCheck" ) // Check represents an interface for performing a check on a peer. @@ -103,7 +104,6 @@ func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { pc.Checks = make([]Check, 0, len(rawChecks)) for name, rawCheck := range rawChecks { - //nolint:gocritic switch name { case NBVersionCheckName: check := &NBVersionCheck{} @@ -111,6 +111,12 @@ func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { return err } pc.Checks = append(pc.Checks, check) + case OSVersionCheckName: + check := &OSVersionCheck{} + if err := json.Unmarshal(rawCheck, check); err != nil { + return err + } + pc.Checks = append(pc.Checks, check) } } return nil diff --git a/management/server/posture/version.go b/management/server/posture/nb_version.go similarity index 93% rename from management/server/posture/version.go rename to management/server/posture/nb_version.go index 1fe0aa57389..0645b8f73e0 100644 --- a/management/server/posture/version.go +++ b/management/server/posture/nb_version.go @@ -29,10 +29,8 @@ func (n *NBVersionCheck) Check(peer nbpeer.Peer) (bool, error) { } log.Debugf("peer %s NB version %s is older than minimum allowed version %s", - peer.ID, - peer.Meta.UIVersion, - n.MinVersion, - ) + peer.ID, peer.Meta.WtVersion, n.MinVersion) + return false, nil } diff --git a/management/server/posture/version_test.go b/management/server/posture/nb_version_test.go similarity index 100% rename from management/server/posture/version_test.go rename to management/server/posture/nb_version_test.go diff --git a/management/server/posture/os_version.go b/management/server/posture/os_version.go new file mode 100644 index 00000000000..b3cae5478c0 --- /dev/null +++ b/management/server/posture/os_version.go @@ -0,0 +1,96 @@ +package posture + +import ( + "github.com/hashicorp/go-version" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + log "github.com/sirupsen/logrus" +) + +type MinVersionCheck struct { + MinVersion string +} + +type MinKernelVersionCheck struct { + MinKernelVersion string +} + +type OSVersionCheck struct { + Android *MinVersionCheck + Darwin *MinVersionCheck + Ios *MinVersionCheck + Linux *MinKernelVersionCheck + Windows *MinKernelVersionCheck +} + +var _ Check = (*OSVersionCheck)(nil) + +func (c *OSVersionCheck) Check(peer nbpeer.Peer) (bool, error) { + peerGoOS := peer.Meta.GoOS + switch peerGoOS { + case "android": + return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Android) + case "darwin": + return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Darwin) + case "ios": + return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Ios) + case "linux": + return checkMinKernelVersion(peerGoOS, peer.Meta.KernelVersion, c.Linux) + case "windows": + return checkMinKernelVersion(peerGoOS, peer.Meta.KernelVersion, c.Windows) + } + return true, nil +} + +func (c *OSVersionCheck) Name() string { + return OSVersionCheckName +} + +func checkMinVersion(peerGoOS, peerVersion string, check *MinVersionCheck) (bool, error) { + if check == nil { + log.Debugf("peer %s OS is not allowed in the check", peerGoOS) + return false, nil + } + + peerNBVersion, err := version.NewVersion(peerVersion) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + check.MinVersion) + if err != nil { + return false, err + } + + if constraints.Check(peerNBVersion) { + return true, nil + } + + log.Debugf("peer %s OS version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinVersion) + + return false, nil +} + +func checkMinKernelVersion(peerGoOS, peerVersion string, check *MinKernelVersionCheck) (bool, error) { + if check == nil { + log.Debugf("peer %s OS is not allowed in the check", peerGoOS) + return false, nil + } + + peerNBVersion, err := version.NewVersion(peerVersion) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + check.MinKernelVersion) + if err != nil { + return false, err + } + + if constraints.Check(peerNBVersion) { + return true, nil + } + + log.Debugf("peer %s kernel version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinKernelVersion) + + return false, nil +} diff --git a/management/server/posture/os_version_test.go b/management/server/posture/os_version_test.go new file mode 100644 index 00000000000..8e1f66e67d6 --- /dev/null +++ b/management/server/posture/os_version_test.go @@ -0,0 +1,120 @@ +package posture + +import ( + "testing" + + "github.com/netbirdio/netbird/management/server/peer" + + "github.com/stretchr/testify/assert" +) + +func TestOSVersionCheck_Check(t *testing.T) { + tests := []struct { + name string + input peer.Peer + check OSVersionCheck + wantErr bool + isValid bool + }{ + { + name: "Valid Peer Linux Kernel version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + KernelVersion: "6.1.1", + }, + }, + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{ + MinKernelVersion: "6.0.0", + }, + }, + wantErr: false, + isValid: true, + }, + { + name: "Not valid Peer macOS version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "darwin", + OSVersion: "14.2.1", + }, + }, + check: OSVersionCheck{ + Darwin: &MinVersionCheck{ + MinVersion: "15", + }, + }, + wantErr: false, + isValid: false, + }, + { + name: "Valid Peer ios version allowed by any rule", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "ios", + OSVersion: "17.0.1", + }, + }, + check: OSVersionCheck{ + Ios: &MinVersionCheck{ + MinVersion: "0", + }, + }, + wantErr: false, + isValid: true, + }, + { + name: "Valid Peer android version not allowed by rule", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "android", + OSVersion: "14", + }, + }, + check: OSVersionCheck{}, + wantErr: false, + isValid: false, + }, + { + name: "Valid Peer Linux Kernel version not allowed by rule", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + KernelVersion: "6.1.1", + }, + }, + check: OSVersionCheck{}, + wantErr: false, + isValid: false, + }, + { + name: "Invalid Peer Linux kernel version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + KernelVersion: "x.y.1", + }, + }, + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{ + MinKernelVersion: "6.0.0", + }, + }, + wantErr: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isValid, err := tt.check.Check(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.isValid, isValid) + }) + } +} From c21bb7746cd219411bead8986fa900170fec5fbf Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Wed, 31 Jan 2024 13:16:02 +0100 Subject: [PATCH 14/35] Initial support of Geolocation service (#1491) --- .gitignore | 3 +- client/cmd/testutil.go | 3 +- client/internal/engine_test.go | 3 +- go.mod | 1 + go.sum | 4 +- infrastructure_files/download-geolite2.sh | 62 ++++++ management/client/client_test.go | 3 +- management/cmd/management.go | 15 +- management/server/account.go | 6 +- management/server/account_test.go | 2 +- management/server/dns_test.go | 2 +- management/server/geolocation/geolocation.go | 189 ++++++++++++++++++ .../server/geolocation/geolocation_test.go | 49 +++++ management/server/management_proto_test.go | 2 +- management/server/management_test.go | 2 +- management/server/nameserver_test.go | 2 +- management/server/route_test.go | 2 +- .../server/testdata/GeoLite2-City-Test.mmdb | Bin 0 -> 21117 bytes 18 files changed, 333 insertions(+), 17 deletions(-) create mode 100755 infrastructure_files/download-geolite2.sh create mode 100644 management/server/geolocation/geolocation.go create mode 100644 management/server/geolocation/geolocation_test.go create mode 100644 management/server/testdata/GeoLite2-City-Test.mmdb diff --git a/.gitignore b/.gitignore index c4f90b84723..1467eebacb9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store -*.db \ No newline at end of file +*.db +GeoLite2-City* \ No newline at end of file diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 28423776fde..cba47326f84 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -78,8 +78,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste if err != nil { return nil, nil } - accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", - eventStore, false) + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index 5dfc171a632..875dc60036c 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1049,8 +1049,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) { if err != nil { return nil, "", err } - accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", - eventStore, false) + accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) if err != nil { return nil, "", err } diff --git a/go.mod b/go.mod index 5ed6c040f16..55a79be446c 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/netbirdio/management-integrations/additions v0.0.0-20240118163419-8a7c87accb22 github.com/netbirdio/management-integrations/integrations v0.0.0-20240118163419-8a7c87accb22 github.com/okta/okta-sdk-golang/v2 v2.18.0 + github.com/oschwald/maxminddb-golang v1.12.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 github.com/pion/stun/v2 v2.0.0 diff --git a/go.sum b/go.sum index b24b6ddc9e9..c2177f90376 100644 --- a/go.sum +++ b/go.sum @@ -407,6 +407,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= +github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -1000,4 +1002,4 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= \ No newline at end of file +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/infrastructure_files/download-geolite2.sh b/infrastructure_files/download-geolite2.sh new file mode 100755 index 00000000000..2bd1c09045e --- /dev/null +++ b/infrastructure_files/download-geolite2.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# set $MM_ACCOUNT_ID and $MM_LICENSE_KEY when calling this script +# see https://dev.maxmind.com/geoip/updating-databases#directly-downloading-databases + +# Check if MM_ACCOUNT_ID is set +if [ -z "$MM_ACCOUNT_ID" ]; then + echo "MM_ACCOUNT_ID is not set. Please set the environment variable." + exit 1 +fi + +# Check if MM_LICENSE_KEY is set +if [ -z "$MM_LICENSE_KEY" ]; then + echo "MM_LICENSE_KEY is not set. Please set the environment variable." + exit 1 +fi + +# to install sha256sum on mac: brew install coreutils +if ! command -v sha256sum &> /dev/null +then + echo "sha256sum is not installed or not in PATH, please install with your package manager. e.g. sudo apt install sha256sum" > /dev/stderr + exit 1 +fi + +DATABASE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz" +SIGNATURE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" + +# Download the database and signature files +echo "Downloading database file..." +DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") +echo "Downloading signature file..." +SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") + +# Verify the signature +echo "Verifying signature..." +if sha256sum -c --status "$SIGNATURE_FILE"; then + echo "Signature is valid." +else + echo "Signature is invalid. Aborting." + exit 1 +fi + +# Unpack the database file +EXTRACTION_DIR=$(basename "$DATABASE_FILE" .tar.gz) +echo "Unpacking $DATABASE_FILE..." +mkdir -p "$EXTRACTION_DIR" +tar -xzvf "$DATABASE_FILE" + +# Create a SHA256 signature file +MMDB_FILE="GeoLite2-City.mmdb" +cd "$EXTRACTION_DIR" +sha256sum "$MMDB_FILE" > "$MMDB_FILE.sha256" +echo "SHA256 signature created for $MMDB_FILE." +cd - + +# Remove downloaded files +rm "$DATABASE_FILE" "$SIGNATURE_FILE" + +# Done. Print next steps +echo "Process completed successfully." +echo "Now you can place $EXTRACTION_DIR/$MMDB_FILE to 'datadir' of management service." +echo -e "Example:\n\tdocker compose cp $EXTRACTION_DIR/$MMDB_FILE management:/var/lib/netbird/" diff --git a/management/client/client_test.go b/management/client/client_test.go index cd15bf7be11..fafc22e4432 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -60,8 +60,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { peersUpdateManager := mgmt.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} - accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", - eventStore, false) + accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "", eventStore, nil, false) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index 2b8bdb7ad24..82a897ffda5 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -7,7 +7,6 @@ import ( "errors" "flag" "fmt" - "github.com/netbirdio/management-integrations/integrations" "io" "io/fs" "net" @@ -29,9 +28,11 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/keepalive" + "github.com/netbirdio/management-integrations/integrations" "github.com/netbirdio/netbird/encryption" mgmtProto "github.com/netbirdio/netbird/management/proto" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/geolocation" httpapi "github.com/netbirdio/netbird/management/server/http" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -159,8 +160,15 @@ var ( } } + geo, err := geolocation.NewGeolocation(config.Datadir) + if err != nil { + log.Warnf("could not initialize geo location service, we proceed without geo support") + } else { + log.Infof("geo location service has been initialized from %s", config.Datadir) + } + accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain, - dnsDomain, eventStore, userDeleteFromIDPEnabled) + dnsDomain, eventStore, geo, userDeleteFromIDPEnabled) if err != nil { return fmt.Errorf("failed to build default manager: %v", err) } @@ -284,6 +292,9 @@ var ( SetupCloseHandler() <-stopCh + if geo != nil { + _ = geo.Stop() + } ephemeralManager.Stop() _ = appMetrics.Close() _ = listener.Close() diff --git a/management/server/account.go b/management/server/account.go index 101790c6389..cd8a8a5cc6a 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -27,6 +27,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" nbpeer "github.com/netbirdio/netbird/management/server/peer" @@ -138,6 +139,7 @@ type DefaultAccountManager struct { externalCacheManager ExternalCacheManager ctx context.Context eventStore activity.Store + geo *geolocation.Geolocation // singleAccountMode indicates whether the instance has a single account. // If true, then every new user will end up under the same account. @@ -816,10 +818,12 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) { // BuildManager creates a new DefaultAccountManager with a provided Store func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager, - singleAccountModeDomain string, dnsDomain string, eventStore activity.Store, userDeleteFromIDPEnabled bool, + singleAccountModeDomain string, dnsDomain string, eventStore activity.Store, geo *geolocation.Geolocation, + userDeleteFromIDPEnabled bool, ) (*DefaultAccountManager, error) { am := &DefaultAccountManager{ Store: store, + geo: geo, peersUpdateManager: peersUpdateManager, idpManager: idpManager, ctx: context.Background(), diff --git a/management/server/account_test.go b/management/server/account_test.go index 44ca770a74f..b1f2300889d 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2236,7 +2236,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.cloud", eventStore, nil, false) } func createStore(t *testing.T) (Store, error) { diff --git a/management/server/dns_test.go b/management/server/dns_test.go index bff0c987845..aac35308c93 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -193,7 +193,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "netbird.test", eventStore, nil, false) } func createDNSStore(t *testing.T) (Store, error) { diff --git a/management/server/geolocation/geolocation.go b/management/server/geolocation/geolocation.go new file mode 100644 index 00000000000..4461bc1865f --- /dev/null +++ b/management/server/geolocation/geolocation.go @@ -0,0 +1,189 @@ +package geolocation + +import ( + "bytes" + "crypto/sha256" + "fmt" + "io" + "net" + "os" + "path" + "sync" + "time" + + "github.com/oschwald/maxminddb-golang" + log "github.com/sirupsen/logrus" +) + +const mmdbFileName = "GeoLite2-City.mmdb" + +type Geolocation struct { + mmdbPath string + mux *sync.RWMutex + sha256sum []byte + db *maxminddb.Reader + stopCh chan struct{} + reloadCheckInterval time.Duration +} + +type Record struct { + City struct { + GeonameID uint `maxminddb:"geoname_id"` + Names struct { + En string `maxminddb:"en"` + } `maxminddb:"names"` + } `maxminddb:"city"` + Continent struct { + GeonameID uint `maxminddb:"geoname_id"` + Code string `maxminddb:"code"` + } `maxminddb:"continent"` + Country struct { + GeonameID uint `maxminddb:"geoname_id"` + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` +} + +func NewGeolocation(datadir string) (*Geolocation, error) { + mmdbPath := path.Join(datadir, mmdbFileName) + + db, err := openDB(mmdbPath) + if err != nil { + return nil, err + } + + sha256sum, err := getSha256sum(mmdbPath) + if err != nil { + return nil, err + } + + geo := &Geolocation{ + mmdbPath: mmdbPath, + mux: &sync.RWMutex{}, + sha256sum: sha256sum, + db: db, + reloadCheckInterval: 60 * time.Second, // TODO: make configurable + stopCh: make(chan struct{}), + } + + go geo.reloader() + + return geo, nil +} + +func openDB(mmdbPath string) (*maxminddb.Reader, error) { + _, err := os.Stat(mmdbPath) + + if os.IsNotExist(err) { + return nil, fmt.Errorf("%v does not exist", mmdbPath) + } else if err != nil { + return nil, err + } + + db, err := maxminddb.Open(mmdbPath) + if err != nil { + return nil, fmt.Errorf("%v could not be opened: %w", mmdbPath, err) + } + + return db, nil +} + +func getSha256sum(mmdbPath string) ([]byte, error) { + f, err := os.Open(mmdbPath) + if err != nil { + return nil, err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + + return h.Sum(nil), nil +} + +func (gl *Geolocation) Lookup(ip string) (*Record, error) { + gl.mux.RLock() + defer gl.mux.RUnlock() + + parsedIp := net.ParseIP(ip) + if parsedIp == nil { + return nil, fmt.Errorf("could not parse IP %s", ip) + } + + var record Record + err := gl.db.Lookup(parsedIp, &record) + if err != nil { + return nil, err + } + + return &record, nil +} + +func (gl *Geolocation) Stop() error { + close(gl.stopCh) + if gl.db != nil { + return gl.db.Close() + } + return nil +} + +func (gl *Geolocation) reloader() { + for { + select { + case <-gl.stopCh: + return + case <-time.After(gl.reloadCheckInterval): + newSha256sum1, err := getSha256sum(gl.mmdbPath) + if err != nil { + log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err) + continue + } + if !bytes.Equal(gl.sha256sum, newSha256sum1) { + // we check sum twice just to avoid possible case when we reload during update of the file + // considering the frequency of file update (few times a week) checking sum twice should be enough + time.Sleep(50 * time.Millisecond) + newSha256sum2, err := getSha256sum(gl.mmdbPath) + if err != nil { + log.Errorf("failed to calculate sha256 sum for '%s': %s", gl.mmdbPath, err) + continue + } + if !bytes.Equal(newSha256sum1, newSha256sum2) { + log.Errorf("sha256 sum changed during reloading of '%s'", gl.mmdbPath) + continue + } + err = gl.reload(newSha256sum2) + if err != nil { + log.Errorf("reload failed: %s", err) + } + } else { + log.Debugf("No changes in '%s', no need to reload. Next check is in %.0f seconds.", + gl.mmdbPath, gl.reloadCheckInterval.Seconds()) + } + } + } +} + +func (gl *Geolocation) reload(newSha256sum []byte) error { + gl.mux.Lock() + defer gl.mux.Unlock() + + log.Infof("Reloading '%s'", gl.mmdbPath) + + err := gl.db.Close() + if err != nil { + return err + } + + db, err := openDB(gl.mmdbPath) + if err != nil { + return err + } + + gl.db = db + gl.sha256sum = newSha256sum + + log.Infof("Successfully reloaded '%s'", gl.mmdbPath) + + return nil +} diff --git a/management/server/geolocation/geolocation_test.go b/management/server/geolocation/geolocation_test.go new file mode 100644 index 00000000000..8d4c71599be --- /dev/null +++ b/management/server/geolocation/geolocation_test.go @@ -0,0 +1,49 @@ +package geolocation + +import ( + "os" + "path" + "testing" + + "github.com/netbirdio/netbird/util" + "github.com/stretchr/testify/assert" +) + +// from https://github.com/maxmind/MaxMind-DB/blob/main/test-data/GeoLite2-City-Test.mmdb +var mmdbPath = "../testdata/GeoLite2-City-Test.mmdb" + +func TestGeoLite_Lookup(t *testing.T) { + tempDir := t.TempDir() + filename := path.Join(tempDir, mmdbFileName) + err := util.CopyFileContents(mmdbPath, filename) + assert.NoError(t, err) + defer func() { + err := os.Remove(filename) + if err != nil { + t.Errorf("os.Remove: %s", err) + } + }() + + geo, err := NewGeolocation(tempDir) + assert.NoError(t, err) + assert.NotNil(t, geo) + defer func() { + err = geo.Stop() + if err != nil { + t.Errorf("geo.Stop: %s", err) + } + }() + + record, err := geo.Lookup("89.160.20.128") + assert.NoError(t, err) + assert.NotNil(t, record) + assert.Equal(t, "SE", record.Country.ISOCode) + assert.Equal(t, uint(2661886), record.Country.GeonameID) + assert.Equal(t, "Linköping", record.City.Names.En) + assert.Equal(t, uint(2694762), record.City.GeonameID) + assert.Equal(t, "EU", record.Continent.Code) + assert.Equal(t, uint(6255148), record.Continent.GeonameID) + + _, err = geo.Lookup("589.160.20.128") + assert.Error(t, err) +} diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index 21e9862f054..e5457db0211 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -413,7 +413,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) peersUpdateManager := NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "", - eventStore, false) + eventStore, nil, false) if err != nil { return nil, "", err } diff --git a/management/server/management_test.go b/management/server/management_test.go index ee9641a8c17..f4535487764 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -504,7 +504,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { peersUpdateManager := server.NewPeersUpdateManager(nil) eventStore := &activity.InMemoryEventStore{} accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "", - eventStore, false) + eventStore, nil, false) if err != nil { log.Fatalf("failed creating a manager: %v", err) } diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 791dc567762..70b4b251960 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -748,7 +748,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, nil, false) } func createNSStore(t *testing.T) (Store, error) { diff --git a/management/server/route_test.go b/management/server/route_test.go index bbf0ea3dd13..a5db2ca07b4 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1014,7 +1014,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { return nil, err } eventStore := &activity.InMemoryEventStore{} - return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, false) + return BuildManager(store, NewPeersUpdateManager(nil), nil, "", "", eventStore, nil, false) } func createRouterStore(t *testing.T) (Store, error) { diff --git a/management/server/testdata/GeoLite2-City-Test.mmdb b/management/server/testdata/GeoLite2-City-Test.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..bfbaa094590ec9df1bab022701b930aa7f212f56 GIT binary patch literal 21117 zcmZ{q349bq_Qz|wdjLTZ0YT6}2LlWUsNjVsIVY2Z5OWZeU>uSm8AxX096}qDJ z4!INVLnPdXTsrD{J8`{Nch!&tbXQ$(*X#eQ_qsC^|NnpZFyF3MRo$&x04Ib{g|LK$&^TpfbYOI3bV89Qg}8)Mof%yimohG6bY)!5=*GB$(VfwQ(UWl{ zqZgw$<0?iUMqkF&jB6M%jDC#%44o0n7{Iue5yu$F7{nONurY=(hBB^W3}Xyuj9^^P zxPfsaV!Adl+LF@r(pUA|r{B%t&F_8L12h zBaM;H$Y6|RWHQDvvKaSb_!kI~&8ZwlE+daIo{`U(z_^cbKVu@JfH8^j0HcucAj2ty zcvpzYz$_t(SXj((LHMW;Q#e(ER4G}pv}896rxK(Lev$oG7hF`Y4kQAwZ&RdMx4gqRr-{)L3rmxXvNBCKX1w5x%FuL@BMtPx^16%lhH zCFk-z=Ls>N^DqW@7(H*H5Q`$I#X>BJq?U4#WsK!Qtl;X8M~Xbbc`F&Kgjh{^oi=dE zC!x||A=YwgU8LT6A&ep$Id2nVGoy}CPar+F2(gug+akj4LhNASPR6cC-fkiGL{fWM z!KZ}S2bB&A@w5<~dke8&h%OrEH82hkx?r{*qUy4g)Zhr?Xyl&9xWaKEkZtzoNg@vhaN&K4_~aJ{02b$ZHnjqloZhF8L3}KN+7e zK4pByXkwfv$OpCHW?u-=N;eZ17#A6z6J#k<-G2%3Z_fLY@l_=6Ya#x_!fzPgGQNux z`JSKtgAhM*-cO95BSrqpMSdY%)W}(w$RKPCmUS<4Q&^LR6p0B7NF~ zmcG<5%hkei4OfU^^kejol+?LotgsB=ylWY8ks<@Bh$Y)Go+{fVfXPHPT*%OumzUL&y1Hw|sB_Cus+e*qO6rln~CrdF` za0$y4E?Lr6(o!lcZWd0B2s!>)IR076g~h`~yo?G$S4=GVARoie2oOl!ps+j~Nlh1) z8C;~2QN?(KF_ZBZ0#$!hSRRX{s)c117paK|Yq?>wg=G%s&5h*E!-Ul}hkUS_<)c$M)Q<8{Uxj5i6=dZdDl zjJL35fi3-wQ*SfgVZ6&g{F4>lXMDi;knwlMN4N_w%q$-Z%RgB7PsS&VPZ^(~q)%9y zfJwq~9@FsO!qUunEsR#i1;$0j=Zr5H|02MgNV6{)U*Vqc&|d?Ju>1$N{y}C7%QuW~ zh2=Xg`8}a~0;drFEWZfLPn`Oh@n0@t^yjZA@`tee#(BR-=P80xiiOaFjaE^iM0cTR zKqsMCxkv{_M}qNm8A- zP_E=6z1kJIiiLgJ39m+lenPp1^I|xU8?E$bq28{-091$<%C*2qp~L~#31uK)6Urd2 zFu2`ahOlrb!R#KyKjnI%4ClNN?JC^B!W&u0W2B5ih1-R46L70gZssDlv@3EO3rDvT z-hm2t3*}DEyNmOT_T9t6G3|tSo{}k)L?BHlNq}7_$y_ppOByPrvd|GNl;)#y z5#bCLRx+xD@(ATAGuw*DJMpwv9%~aS)k2xYC2JV9jMc|9RHL> zLRpLxoNYo`f}Y>Vx^VncIQ}Ud|CAL%c^pMJ#wkw-WhFmh72(Q7oLYk->xA+or`8gB z!JwgmvYxSlg<(!@!qW!|Wi#siLnw8?S)tSe4MN!h>=w#aV24n)afR)K-j{G{Cu0{2 z4|8e{D(n-=UQRv5c^`4=X~urSRh67NfC|Tjg7~K#5y~MJ^3D2meTrLU|5&K`0^4d!EqO=%p7~_*cT!#(a1g z6wJX=VoM5P=Uctid1oLJ*e(IGZ)U&uqZ``GyP_F{67HS_B_H9@1 z8WzS7jCrK?M}+}G)j2PgU<|m*2ByZ5(A0VmD!7C?7)TMS4Y*aPLx3BEIusZ#)a&?W z!w9imI5mQCJ;9iN>W!#ylTb%;9=i)O?`9U>LNF$tdK)U-Bh=BrokG2xi`+plY)QR~ zh3u1K@8r}NR7ezRJf{)}v215*5(|^t3GJvbUZ{wF>Nue~fDECgamjRoIUHkIm`N}P z4x^&x2=!jh%O(tfkA&^2xr{u5+1h+mctEHVfQdrAkBi(-FdtOF!bt?PTMJQPvQQu7 zJSV}tXAujF+X<(j!W^NN01pcl@lW*%)eV#hbt+ew)~-T13q1t0&ni&CFVu%P&)2R( zfQ3PV;ce9EsPL#zX8?}~wUUce5w10QX(kK*!u57?>M>NP5o$H3W)aMhu4UnDg4yV~ zsIW??^MIv7oewM$>H;pguw6;SKRFyr2u7?@m!ZPrLS4>zE810ff`uyym=#ogH7aZr zD#lh_C)6idxRzkfBlhg-1`?V*DC1wfP&adhI)Z6ETUfZ2VBQ7sPunfj9RTJSx<~z7 zsJno_3UxPdT&R11147*kJS|j?f9gKEi#dJw^Q{{QamIrVqQViOa{N;dQ{F(BhU}iB zjAI1TYn(uZb3#1{oE9p_KlKz9G26iLPd!UQBbchsqQdh+eU9%LYFFU}7QRR@HG2sa z{w~y)fwzVF3eYIjSAjQ#`WjbwonTI`H(3}a4E&8#Z!tLjnbYYV+~s|tzRP*<5zM>~ zSok3ezvk3OsL&+TkAY8w`VSWVlVHSY^-~spMlgNWc~oc>YBT4x5c(MRyuiYX1apkO zzz}y9>c3F$7oq+e_)e%_0{;=}S6qRsn-k$17Jf@G+weUq{3O&LIPXV-IZ1wI;eQEc zU;m27szODdX{h=;7x{x=Xr@^xr746#+!PI^wT?owa$X05(RQs93ojuUPuIGjLO-Eh z3iKA*Wk64%bp@^v+T~oK8^L^RcNX?&cb6+!*sER1t8kaAh1Q4j`Vt1u=hQWf7=o!w ze^eMGG#$8BXt7*m0Ks-Yr{Wj`2}bW~gHd6q&}^JHgkVfLPfg@J{I~3rY=ELm?5-> zId3|l|2$4rGO7qeIZvC33bjJ}3s5bzN4dyj1amsgVqp!zG`HEPFi&W6IBzb&G*iR$ z7m(0!huR`kSSPf_z!O4S0xTEWQm(#?U_@<=E5}GdPS={9%p*;t@AT*AD+Vkz|y+|qTuLN@#Uq*%3gogNskgUB* zFs=1<7QR6+RSKiRKZVu^yf3u3fOmxUH!k@$!MydmEPRh(tTnU`P~jt?eaLx#Cz#Ls zn1%l!nB(^eDzpmiQ{cSNKI0-y1k-k#S=d4_pMC)qz7X0)&ikBT&f9;n@ZVgaBd5Ls zx(n@V)cZqd{{emy+Bd-WLi?7Bd`Gx0i&H-^ek7Q;{uvd1721C}?-zop=Wi_honUPB ztOBVn!fFBV_E@1jt4c7sz=|@~PQu!O^H^QudDcr<*tuPWOOd)ONG5>n~H znhc~0YYG>!6O8#_b+9mv(C-~iWdL~9H8@S4mI)Cg+;ctlu(0FF8z1~72b38ocP zvapIU+%Tex6YBfLDF1sEBnn!8AUOf7Z1mH1%AM)Fxrw zz!f$U%>LZW!a9OkVGB~bg>@^iLs++Qk?jPto?&OZNI3j0F0u#MC#-up^%TME3B*4+ z4*LlsjD9(R)B!0M)`N^gjKhp0jHAMOjH}C%$61KW_J6HOMeac5XK5uaug6*LDs&fz zY_-YB^4kAtg3Du1@Xnax^%XmcyqI8d#b5+UsG*uQ|*f6ioo!MY~=fb8R1pogN@bU zGvTx0hUBW!;fa|kEl=%iIegGw=&x6&CbcMV5ccV;xz zY-_4{x~X^y$(RTrbba zd5fF@x7TxioU^DX=yMiT7W$mU?x4TEIJFEpfnc$#bd2K@>TgGx*Hglxv)}OuI0A0; zca_)U8cUt$x<13}DfW6&D!l%Hv+PrSRN}Y^H)i`>C2oHJZ7oLgsYlMEM@Dqsk?Zys zx;=$1+3_x?r!eS&GM0||u7s3q>Y=n0pVKqV=#&(hD01ofcDiso^@7+7x-+k-c2!gD z?xxy9XnymY1I>>wM%(lNl?v^2>N7LL+t34OedAoC{Vj)9p-hH9IJwyUu-lKP`V~zM zQ{k>x^<9a{nRdO`c|FFS>?tX8dW!Y@G>5!G8+>?bT09ainQ-ZW0mZW3$d8<;m!FWX z_iDoM<-7a=J;CLh<}Y=l(9B6ncnuVQfrJl3jYZ)TdU!?n2(&ucI3GGy>x(lkY(IPc z)c)o-e%NL#lm!ZmGMxqBIy=CoSO8>+@ny(r{;wBYhm%!Sc{H4>i-C?JakD zWJ^;ab$UYk(DSL;UU@AWjTyO@X5^-(+C|iKWR{2a$SN4|y2je@DRi`9@foeB*IeBF zRCB{AbVf?R=X3}BpJv1SoF1>IvfLZ=7gqSZGb$||TXf53(9M?LU+>kbC#A4O>oIYY zV0bd&!(=Upn=0p>^ah8TY8T*F4?r|CBfK8-?KB1%zq(Bysjt=Rsb}hoWyd(4ZOpvm z^;^&}XOF!z<<0nSdc;=6&W^1k({p`_k;%)Uo=D8giPvMs8g3vb0Mqm9`I*U>R>fXF z`vItt9$J7bzw`rsITK(9we{o(_BJn{f1!RZja%I3`plGYeRx-RIfkqC%u+}PjJ9Y4 zQqkDBd)w=&#^%NCjxC8h%$mi828;F-%^Z!#$cHWSFQ}tfWdQw`3+cPb6v;tmC zj~Td5FTj821gW2#tjC&acG3v0q-2A>*wM0S&BgV5nok^JourZ2HvVoHNlr3#O=?b2 z>2z1IoC+y9e374$6Lb|};0tgO+Eonmm;h(cRI|f;#5_F0G-Er;?3Sf5T)V;tF%3<( z(7Iw%^PJ;6UKw({#@;->?7RN4(vYve<6PfcKKmlJD$8(nIRS6cv{G+bIUDn^$8!xm zlUUF6Bv&xtFDfMm5@WZ!e6ZX~m~(uYi!Hmr5nm>+OFX17>GJ9_d4J8kQ|l5++EM+zC1U!DxKU*s?#^w?TM`$ zTrcfgc1QmnJ@MR;BdI^q)7)ilkDgQNfS3zDVd&CiqaJ@ zUTJDkDNIC1ip)YAFb8ddM{iYA%~mpPY0gojXA^#XW_EZRg0T$9XYud2i~%tB3(F2R zpIOxW^cikkr8K#rJ8%Ew3TW1p~IX+hfToP6_ z?ZRu^i~6p(*YsX-jY)yZ3Rh}56b?9jm96aFvXevP-W)k_|4D9FiC2%YS?<*H zjI?rBz{l&pE$ZmlsvM&auvGFFVjd)_ZFUzlO~=!G~HR| z#yXb6V_Kq_LT}oX@3AUrld$_kbcCOURbpv`YZ)V(k8RQe=~OOs2seU5$;B*dkcQbf zJA8(EkXMM!3+qtYu$Bo+<7ob*jCGdI_zO$Y|%{jOHavTi36@Sbr8}&PP1Q zHLmCJ*t9Had~%wzLawT8+6pYIkO&=u&y-T=;Y?{+v%O`*Q+j}Mz_ydO?Mt{bTD zZQA}QXBYq8iBBH}HHKW>;P%Y%2Y*XPUd}*{P$K?%{p-t9H zqT19aa0+e8&8N;y$#uG?Q}0^8#EML&Xw{0V(0-ZWOle-$&~oAoYzrk|s#|0TfrF46 z0E8x6hi*}-;9y7oNmU*g!Z0&2K!|n z%APLLy;F3KZut|U`Zdd25iE21jI4se&*{#=ujm08=LWyW;jIZigcgbM)vO0py5CT# z&ep*lQ6hBp^V#T-RBSfj!%|bRFPnz{2+{Hqql=$>TgT3DT->p?<@kQ|2U6&d zC&<*~#t?>${^+Pj^~Wv76Y=nZg?o-~e`8{7RVK$)+_q5O8(q@jOLzvGjyzvGZzND+ODQ}}`a;Yri_4Pm3 zp=p`CA4r9dpYAH5rJlTWV);H zV?>BRtYY`pvUpGPu31s-9!Gsnb`WP=R;3#&y>b3BInO%295n>mqhCW$M+V!Z!|!v# zyT#Z#)u89`Q(%{((B3kNqu8FGkXuv)(b~NS#ne7)A9BiJy zCQ7q=s!1Pdq45v$r7~^#0nD1b$ZC?#Mnbob?sE1TE$l)wkpGDRhHA|_Q%%6 z9*XLH+wvN;Dv=jJc3Nn=tEAhQ?haJBe7qM<&I!7!c&+XB`_cR3Xss<9$xB!4y|KjJ zM|4%%=8?&P#wKOQ@b^<$eVrDJd|EdQiJjxl^`?0b#p zq7j|@M)h~Um+psh*?ByXvx9y=?-o+zCBk!lGA_UZ7ND;ka?>fdPT1?)vqLAT7$Qg= zOng@3Ebd>&`LnaJ;cZ!sK+@(`GD#s)?mpuZ+8i^q;r)1)v4zQEyH3vXmeJ$`=@`V0 z3$I)^6wsBqaoI>6MMp5T3!;|3sVE*gOzVt{@Mbt1xnh_vmt-j|hqg4&X^85(w1|2) z8JBGqkni;Qozu~IU6KuT2h~JXb)2ILvfBs#xhf7}u;h=e8&nh3&>Q?sMnfHZ5Mp=W zxQdP)lI0cLObSV4&vO>-u3_Rh55X}TbdQ`fIdE)JhCab4JW2Z%xthW$o1ER-W)OE<7J-0V58HBxyF(Yw);X)%X9TBB!=kCJ|RZ|>nB`I!?hpohp zOCA^ON9an9Osa7WW0^?nKkQ1duKC}ilO_v>ws?xA?z}-cjS*UguyELzrxzAJ)qa%l zenI}lme@MakF6SKD4yyn^JAwkgXN_k7|Kg$e#xdWa_-$nijU2AxkH=bu?!oxC%6z1 zgV;@_C%9c@ULUTd{uv4G&~7B<3_u%jT(Fk3r_-LAV^KXnmg)^}n*~|&Obu&U_!%5e z&62@Bqjl}+sL6mMDvq&f)kZr?$J|L*QI2t}+vA^xrd+lc-JY@QW*i!J zz^?j8<9y6&Y-8lUrVhzj;j`FeA21GgF_p%(EJ3F%YU`IMV@v(SXr-;Q67i>5K4(c# z8d_H;8fb3NEx%*OnBd@r^PL-8o640}r;$XXq50!enh-TVwP@M3vgb{cVZ(*E(G5dt&nrR=X{<6S&zf-1w4Wk84saH>Jy;K4e+?Jj{v6u!9Zm`Wur&g1OP_WxjsMIanP^sN9V!E^EvJZZ{6;9-wb=mI? zft@}+tkdhGEVlD6uNoGc%JC&7&r{;`A@DXk?Z(Xpqy|=TswWiCP5qFUgEOu7lgdFX=qQ_@)?H4sd zsS_T0a8=o5(m@We|1shAoD-2H%XnvTuqvvFgT@^&nwZSTruH0cyx5Q93UNK(p6su1 zh7Mq;=!zm1w(_vEb!2>q?v&w?r)yY{$@wMC$#CH~ly6w{}YbMfx#RNIoh2WGWgYr&H@E;bkJj+Xr^(YTg1N841y8Y>b@&4FkONq8}bwP_g^ zxV9eBWK1`wExQiWI+xv?JvVg9H_he7Npfxw7jj16;vpzX)k$=2ttm@mybKCo}^=DhWh`QX5*zGpJ+dwULv?plAn z(Y5isOiAbCTH}3~JwE6UNQ20?_52AP8y`eU1|!7%8aZ*v%%S+77A&cuSw=u35~*`FM=9M_N(OcMQ#C;P3MNLiBPxuOe}LJ=vI* zcD|G91aPt?RSt=FW8@&u1^LbmB#v7N$u|tB(?F&lR*z4{nQyB8& z3x+9Va;K(ey73N#c4UZ3>6xxTDW|ZWf-9HHLOI#8oR$9J2{@*f$GNZxjQ!dCgM!V(u7$N_pJ@=+7ETGIcBk|?( ztpg5!Wh*|3?&pbedzo$=~<@Wm)zvl)~#a z>8Qu%ID^IR;qgA_WO88{l-4N?tIaenNE$!~kK>ZM-|0m?oJY&KcZ4*T?_;UMpTLFL z;V2)fpNu(c-eKk>EsWE2v`W4+#9TDjddq34aFFBBxR!%!(Mni*%c2E%OWbDX(nX`8 zDb?*7n(eN17Rw#;Rd*ORBqwFzFBxcPwizj1cvV81yBv8Xr(~adZ|flCCzvloF-o&a5SHoii2ENx$I!`lL(x%aRK;jqog!=vl6W?q$0gw2=sB}w>omOJqkf_o~? zf2H@T#Iq-We>%Yp&4SIl*uho8jwfB#y6w#QQ|rzjf2Q>eRuw04{!8mYX%9`cSgx~m zucq^?3DQBrI*hxd8PCMC+MFTni6^$1u#D#WSISQl8djV?wI=$2*w*zf*}pfM{hKNK zrwAVp_}mrVGP%p_`=epk^4MbdZA%Q_=-?Gdii1z8V(bnd_L%bfjl8H`1P=eXfb4p{ zqQwXrB;UWnV2r&otllh$LqxgHFkGUsIBcDNKn9;EzbJPVSd{Cg=4RMQl4rxxIIjLR z%G^7BUxv|(=l$f^ct2*I(_Jh_48bQY9tUnnPemd>-AGi@QFjKXLi>wlCT*jjIzCRK z_$faII^I+(cl}W-ck>QYL-LKoc=|eoXR$mSKa7KW+Cyc?fLgz)WzmM#)pMiFK4}a! zNe+ws9A8#B=vX+xISrq4*shUl!nQ@0=Qr6f9*u&icWmS>$uN`45s5LKV{(me9AuVm zND;&G`4TaSO)B!uLvCmR)%BBCM(pDEbJ&C#26=JEYP_0;L7AtKd@uRk#GmdpbJEq} zP53-&Ap#1HJP_^VmPUS`a|ACDG2RWxlpYzcIrZ>9yj8@fI&juCGA2%FGvy1jR^n@m zi}-S9)!BA-+(Etw9FtD{h4{#?ZD0#dX~Qvc*=_civUjfRv9aVUQsaH)E_Qr&zLeg% z-$!zd=fitaWJRr*@V&?XS}~!N`5}yK2Hu96JDLp4IQaz#eF)Pwz_#ulEcg3Zu+^eL zy$>I2Jxt!L-?^xnX6ta9G1HRx-Gd_upX=ZY1|JSS>`5*UUFAEH@MavBMbaTBVZB9S zc}Oom!)U6-7du#+?l*m~t^W$?C00hcU3^b;@np2y%`p0~#MUV@w(4Hv+$j;CjbY=4 z7X;|f{!+JRp7v&5U-!hoq@t~=Txt+@L`uvemPgK{5cn2Dz@d;rB4pJ z%ZdwK72cv!O>u4LRXn!X8E{T^`dx+c%Y?CZmp21}edO>&e8rhw?D7}E&C9P6<+q!Y zhMC#A%+_<|hjmFtecd+2InAY`gd=ODF7G-_hlaXqhO-=R<@Ac64<91JLwG%9l|$0p s6>Tbaa3Cy{1YtRTi>6Oc^Wbw({xxp!qs|c%tLchDpRe#O@07IwwjQ{`u literal 0 HcmV?d00001 From b1a43d44fd285183c566fe6c279aa77e47a3507f Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 1 Feb 2024 13:54:30 +0300 Subject: [PATCH 15/35] Add Geo Location posture check (#1500) * wip: implement geolocation check * add geo location posture checks to posture api * Merge branch 'feature/posture-checks' into geo-posture-check * Remove CityGeoNameID and update required fields in API * Add geoLocation checks to posture checks handler tests * Implement geo location-based checks for peers * Update test values and embed location struct in peer system * add support for country wide checks * initialize country code regex once --- management/server/http/api/openapi.yml | 36 ++- management/server/http/api/types.gen.go | 30 +++ .../server/http/posture_checks_handler.go | 64 ++++- .../http/posture_checks_handler_test.go | 131 +++++++++- management/server/peer/peer.go | 6 + management/server/posture/checks.go | 7 + management/server/posture/geo_location.go | 60 +++++ .../server/posture/geo_location_test.go | 224 ++++++++++++++++++ 8 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 management/server/posture/geo_location.go create mode 100644 management/server/posture/geo_location_test.go diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index e7166b0aa73..e35d12ce106 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -842,6 +842,8 @@ components: $ref: '#/components/schemas/NBVersionCheck' os_version_check: $ref: '#/components/schemas/OSVersionCheck' + geo_location_check: + $ref: '#/components/schemas/GeoLocationCheck' NBVersionCheck: description: Posture check for the version of NetBird type: object @@ -884,6 +886,38 @@ components: example: "6.6.12" required: - min_kernel_version + GeoLocationCheck: + description: Posture check for geo location + type: object + properties: + locations: + description: List of geo locations to which the policy applies + type: array + items: + $ref: '#/components/schemas/Location' + action: + description: Action to take upon policy match + type: string + enum: [ "allow", "deny" ] + example: "allow" + required: + - locations + - action + Location: + description: Describe geographical location information + type: object + properties: + country_code: + description: 2-letter ISO 3166-1 alpha-2 code that represents the country + type: string + example: "DE" + city_name: + description: Commonly used English name of the city + type: string + example: "Berlin" + required: + - country_code + - city_name PostureCheckUpdate: type: object properties: @@ -2698,4 +2732,4 @@ paths: '403': "$ref": "#/components/responses/forbidden" '500': - "$ref": "#/components/responses/internal_error" \ No newline at end of file + "$ref": "#/components/responses/internal_error" diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index aed6adaf9cd..34d4ef3ed9e 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -63,6 +63,12 @@ const ( EventActivityCodeUserUnblock EventActivityCode = "user.unblock" ) +// Defines values for GeoLocationCheckAction. +const ( + GeoLocationCheckActionAllow GeoLocationCheckAction = "allow" + GeoLocationCheckActionDeny GeoLocationCheckAction = "deny" +) + // Defines values for NameserverNsType. const ( NameserverNsTypeUdp NameserverNsType = "udp" @@ -178,6 +184,9 @@ type AccountSettings struct { // Checks List of objects that perform the actual checks type Checks struct { + // GeoLocationCheck Posture check for geo location + GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"` + // NbVersionCheck Posture check for the version of operating system NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` @@ -224,6 +233,18 @@ type Event struct { // EventActivityCode The string code of the activity that occurred during the event type EventActivityCode string +// GeoLocationCheck Posture check for geo location +type GeoLocationCheck struct { + // Action Action to take upon policy match + Action GeoLocationCheckAction `json:"action"` + + // Locations List of geo locations to which the policy applies + Locations []Location `json:"locations"` +} + +// GeoLocationCheckAction Action to take upon policy match +type GeoLocationCheckAction string + // Group defines model for Group. type Group struct { // Id Group ID @@ -266,6 +287,15 @@ type GroupRequest struct { Peers *[]string `json:"peers,omitempty"` } +// Location Describe geographical location information +type Location struct { + // CityName Commonly used English name of the city + CityName string `json:"city_name"` + + // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country + CountryCode string `json:"country_code"` +} + // MinKernelVersionCheck Posture check for the version of kernel type MinKernelVersionCheck struct { // MinKernelVersion Minimum acceptable version diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index 123b7e5bc91..d39fcbe7cbb 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -3,6 +3,7 @@ package http import ( "encoding/json" "net/http" + "regexp" "github.com/gorilla/mux" "github.com/rs/xid" @@ -15,6 +16,10 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) +var ( + countryCodeRegex = regexp.MustCompile("^[a-zA-Z]{2}$") +) + // PostureChecksHandler is a handler that returns posture checks of the account. type PostureChecksHandler struct { accountManager server.AccountManager @@ -195,6 +200,10 @@ func (p *PostureChecksHandler) savePostureChecks( }) } + if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { + postureChecks.Checks = append(postureChecks.Checks, toPostureGeoLocationCheck(geoLocationCheck)) + } + if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { util.WriteError(err, w) return @@ -208,7 +217,8 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { return status.Errorf(status.InvalidArgument, "posture checks name shouldn't be empty") } - if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil) { + if req.Checks == nil || (req.Checks.NbVersionCheck == nil && req.Checks.OsVersionCheck == nil && + req.Checks.GeoLocationCheck == nil) { return status.Errorf(status.InvalidArgument, "posture checks shouldn't be empty") } @@ -230,6 +240,25 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { } } + if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { + if geoLocationCheck.Action == "" { + return status.Errorf(status.InvalidArgument, "action for geolocation check shouldn't be empty") + } + if len(geoLocationCheck.Locations) == 0 { + return status.Errorf(status.InvalidArgument, "locations for geolocation check shouldn't be empty") + } + + for _, loc := range geoLocationCheck.Locations { + if loc.CountryCode == "" { + return status.Errorf(status.InvalidArgument, "country code for geolocation check shouldn't be empty") + } + if !countryCodeRegex.MatchString(loc.CountryCode) { + return status.Errorf(status.InvalidArgument, "country code must be 2 letters (ISO 3166-1 alpha-2 format)") + } + } + + } + return nil } @@ -251,6 +280,9 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { Linux: (*api.MinKernelVersionCheck)(osCheck.Linux), Windows: (*api.MinKernelVersionCheck)(osCheck.Windows), } + case posture.GeoLocationCheckName: + geoLocationCheck := check.(*posture.GeoLocationCheck) + checks.GeoLocationCheck = toGeoLocationCheckResponse(geoLocationCheck) } } @@ -261,3 +293,33 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { Checks: checks, } } + +func toGeoLocationCheckResponse(geoLocationCheck *posture.GeoLocationCheck) *api.GeoLocationCheck { + locations := make([]api.Location, 0, len(geoLocationCheck.Locations)) + for _, loc := range geoLocationCheck.Locations { + locations = append(locations, api.Location{ + CityName: loc.CityName, + CountryCode: loc.CountryCode, + }) + } + + return &api.GeoLocationCheck{ + Action: api.GeoLocationCheckAction(geoLocationCheck.Action), + Locations: locations, + } +} + +func toPostureGeoLocationCheck(apiGeoLocationCheck *api.GeoLocationCheck) *posture.GeoLocationCheck { + locations := make([]posture.Location, 0, len(apiGeoLocationCheck.Locations)) + for _, loc := range apiGeoLocationCheck.Locations { + locations = append(locations, posture.Location{ + CountryCode: loc.CountryCode, + CityName: loc.CityName, + }) + } + + return &posture.GeoLocationCheck{ + Action: string(apiGeoLocationCheck.Action), + Locations: locations, + } +} diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index acba6ef5ca1..857950fe0ca 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -106,6 +106,22 @@ func TestGetPostureCheck(t *testing.T) { }, }, } + geoPostureCheck := &posture.Checks{ + ID: "geoPostureCheck", + Name: "geoLocation", + Checks: []posture.Check{ + &posture.GeoLocationCheck{ + Locations: []posture.Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + Action: posture.GeoLocationActionAllow, + }, + }, + } + tt := []struct { name string id string @@ -128,6 +144,13 @@ func TestGetPostureCheck(t *testing.T) { checkName: osPostureCheck.Name, expectedStatus: http.StatusOK, }, + { + name: "GetPostureCheck GeoLocation OK", + expectedBody: true, + id: geoPostureCheck.ID, + checkName: geoPostureCheck.Name, + expectedStatus: http.StatusOK, + }, { name: "GetPostureCheck Not Found", id: "not-exists", @@ -135,7 +158,7 @@ func TestGetPostureCheck(t *testing.T) { }, } - p := initPostureChecksTestData(postureCheck, osPostureCheck) + p := initPostureChecksTestData(postureCheck, osPostureCheck, geoPostureCheck) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { @@ -256,6 +279,45 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Create Posture Checks Geo Location", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "geo_location_check": { + "locations": [ + { + "city_name": "Berlin", + "country_code": "DE" + } + ], + "action": "allow" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + GeoLocationCheck: &api.GeoLocationCheck{ + Locations: []api.Location{ + { + CityName: "Berlin", + CountryCode: "DE", + }, + }, + Action: api.GeoLocationCheckActionAllow, + }, + }, + }, + }, { name: "Create Posture Checks Invalid Check", requestType: http.MethodPost, @@ -301,6 +363,20 @@ func TestPostureCheckUpdate(t *testing.T) { expectedStatus: http.StatusBadRequest, expectedBody: false, }, + { + name: "Create Posture Checks Invalid Geo Location", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "geo_location_check": {} + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, { name: "Update Posture Checks NB Version", requestType: http.MethodPut, @@ -357,6 +433,44 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Update Posture Checks Geo Location", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/geoPostureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "geo_location_check": { + "locations": [ + { + "city_name": "Los Angeles", + "country_code": "US" + } + ], + "action": "allow" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str(""), + Checks: api.Checks{ + GeoLocationCheck: &api.GeoLocationCheck{ + Locations: []api.Location{ + { + CityName: "Los Angeles", + CountryCode: "US", + }, + }, + Action: api.GeoLocationCheckActionAllow, + }, + }, + }, + }, { name: "Update Posture Checks Invalid Check", requestType: http.MethodPut, @@ -424,6 +538,21 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + &posture.Checks{ + ID: "geoPostureCheck", + Name: "geoLocation", + Checks: []posture.Check{ + &posture.GeoLocationCheck{ + Locations: []posture.Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + Action: posture.GeoLocationActionDeny, + }, + }, + }, ) for _, tc := range tt { diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 56462a4a334..6ed2c793853 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -66,6 +66,12 @@ type PeerSystemMeta struct { WtVersion string UIVersion string KernelVersion string + // Location mock location for peer + // TODO: Add actual implementation based on peer IP + Location struct { + CountryCode string + CityName string + } `gorm:"embedded;embeddedPrefix:location_"` } func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 521546851ed..1613cf43f82 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -9,6 +9,7 @@ import ( const ( NBVersionCheckName = "NBVersionCheck" OSVersionCheckName = "OSVersionCheck" + GeoLocationCheckName = "GeoLocationCheck" ) // Check represents an interface for performing a check on a peer. @@ -117,6 +118,12 @@ func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { return err } pc.Checks = append(pc.Checks, check) + case GeoLocationCheckName: + check := &GeoLocationCheck{} + if err := json.Unmarshal(rawCheck, check); err != nil { + return err + } + pc.Checks = append(pc.Checks, check) } } return nil diff --git a/management/server/posture/geo_location.go b/management/server/posture/geo_location.go new file mode 100644 index 00000000000..303c1f1fd25 --- /dev/null +++ b/management/server/posture/geo_location.go @@ -0,0 +1,60 @@ +package posture + +import ( + "fmt" + + nbpeer "github.com/netbirdio/netbird/management/server/peer" +) + +const ( + GeoLocationActionAllow string = "allow" + GeoLocationActionDeny string = "deny" +) + +type Location struct { + // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country + CountryCode string + + // CityName Commonly used English name of the city + CityName string +} + +var _ Check = (*GeoLocationCheck)(nil) + +type GeoLocationCheck struct { + // Locations list of geolocations, to which the policy applies + Locations []Location + + // Action to take upon policy match + Action string +} + +func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) { + for _, loc := range g.Locations { + if loc.CountryCode == peer.Meta.Location.CountryCode { + if loc.CityName == "" || loc.CityName == peer.Meta.Location.CityName { + switch g.Action { + case GeoLocationActionDeny: + return false, nil + case GeoLocationActionAllow: + return true, nil + } + } + } + } + // At this point, no location in the list matches the peer's location + // For action deny and no location match, allow the peer + if g.Action == GeoLocationActionDeny { + return true, nil + } + // For action allow and no location match, deny the peer + if g.Action == GeoLocationActionAllow { + return false, nil + } + + return false, fmt.Errorf("invalid geo location action: %s", g.Action) +} + +func (g *GeoLocationCheck) Name() string { + return GeoLocationCheckName +} diff --git a/management/server/posture/geo_location_test.go b/management/server/posture/geo_location_test.go new file mode 100644 index 00000000000..a2345f09ef0 --- /dev/null +++ b/management/server/posture/geo_location_test.go @@ -0,0 +1,224 @@ +package posture + +import ( + "testing" + + "github.com/netbirdio/netbird/management/server/peer" + + "github.com/stretchr/testify/assert" +) + +func TestGeoLocationCheck_Check(t *testing.T) { + tests := []struct { + name string + input peer.Peer + check GeoLocationCheck + wantErr bool + isValid bool + }{ + { + name: "Peer location matches the location in the allow sets", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "US", + CityName: "Los Angeles", + }, + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + Action: GeoLocationActionAllow, + }, + wantErr: false, + isValid: true, + }, + { + name: "Peer location matches the location in the allow country only", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + }, + }, + Action: GeoLocationActionAllow, + }, + wantErr: false, + isValid: true, + }, + { + name: "Peer location doesn't match the location in the allow sets", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + { + CountryCode: "US", + CityName: "Los Angeles", + }, + }, + Action: GeoLocationActionAllow, + }, + wantErr: false, + isValid: false, + }, + { + name: "Peer location doesn't match the location in the allow country only", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "US", + }, + }, + Action: GeoLocationActionAllow, + }, + wantErr: false, + isValid: false, + }, + { + name: "Peer location matches the location in the deny sets", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + { + CountryCode: "US", + CityName: "Los Angeles", + }, + }, + Action: GeoLocationActionDeny, + }, + wantErr: false, + isValid: false, + }, + { + name: "Peer location matches the location in the deny country only", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Berlin", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + }, + { + CountryCode: "US", + }, + }, + Action: GeoLocationActionDeny, + }, + wantErr: false, + isValid: false, + }, + { + name: "Peer location doesn't match the location in the deny sets", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + { + CountryCode: "US", + CityName: "Los Angeles", + }, + }, + Action: GeoLocationActionDeny, + }, + wantErr: false, + isValid: true, + }, + { + name: "Peer location doesn't match the location in the deny country only", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + Location: Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", + }, + }, + }, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "US", + CityName: "Los Angeles", + }, + }, + Action: GeoLocationActionDeny, + }, + wantErr: false, + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isValid, err := tt.check.Check(tt.input) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.isValid, isValid) + }) + } +} From b0462cd9ad960379ac76e8696c38a32a71e26d37 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 1 Feb 2024 14:49:18 +0300 Subject: [PATCH 16/35] Fix peer meta core compability with older clients (#1515) * Refactor extraction of OSVersion in grpcserver * Ignore lint check --- management/server/grpcserver.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 0e65413a393..617e35d821b 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -254,13 +254,19 @@ func mapError(err error) error { } func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { + osVersion := loginReq.GetMeta().GetOSVersion() + if osVersion == "" { + //nolint:staticcheck + osVersion = loginReq.GetMeta().GetCore() + } + return nbpeer.PeerSystemMeta{ Hostname: loginReq.GetMeta().GetHostname(), GoOS: loginReq.GetMeta().GetGoOS(), Kernel: loginReq.GetMeta().GetKernel(), Platform: loginReq.GetMeta().GetPlatform(), OS: loginReq.GetMeta().GetOS(), - OSVersion: loginReq.GetMeta().GetOSVersion(), + OSVersion: osVersion, WtVersion: loginReq.GetMeta().GetWiretrusteeVersion(), UIVersion: loginReq.GetMeta().GetUiVersion(), KernelVersion: loginReq.GetMeta().GetKernelVersion(), From 3ccdf71bbb868b7050357899c9a35341fd0fdb79 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Tue, 6 Feb 2024 16:36:08 +0300 Subject: [PATCH 17/35] Fix peer meta core compability with older management (#1532) * Revert core field deprecation * fix tests --- management/client/client_test.go | 1 + management/client/grpc.go | 1 + management/proto/management.pb.go | 528 +++++++++++++++--------------- management/proto/management.proto | 3 +- management/server/grpcserver.go | 1 - 5 files changed, 265 insertions(+), 269 deletions(-) diff --git a/management/client/client_test.go b/management/client/client_test.go index fafc22e4432..adb5b3a589a 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -349,6 +349,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { Kernel: info.Kernel, Platform: info.Platform, OS: info.OS, + Core: info.OSVersion, OSVersion: info.OSVersion, WiretrusteeVersion: info.WiretrusteeVersion, KernelVersion: info.KernelVersion, diff --git a/management/client/grpc.go b/management/client/grpc.go index 0037ad9c684..99c6bd47202 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -454,6 +454,7 @@ func infoToMetaData(info *system.Info) *proto.PeerSystemMeta { Hostname: info.Hostname, GoOS: info.GoOS, OS: info.OS, + Core: info.OSVersion, OSVersion: info.OSVersion, Platform: info.Platform, Kernel: info.Kernel, diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 60e41e86883..aca68793670 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -595,12 +595,9 @@ type PeerSystemMeta struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` - GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` - Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` - // core field has been deprecated in favor of the new OsVersion field for better clarity. - // - // Deprecated: Do not use. + Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"` + GoOS string `protobuf:"bytes,2,opt,name=goOS,proto3" json:"goOS,omitempty"` + Kernel string `protobuf:"bytes,3,opt,name=kernel,proto3" json:"kernel,omitempty"` Core string `protobuf:"bytes,4,opt,name=core,proto3" json:"core,omitempty"` Platform string `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"` OS string `protobuf:"bytes,6,opt,name=OS,proto3" json:"OS,omitempty"` @@ -663,7 +660,6 @@ func (x *PeerSystemMeta) GetKernel() string { return "" } -// Deprecated: Do not use. func (x *PeerSystemMeta) GetCore() string { if x != nil { return x.Core @@ -2277,275 +2273,275 @@ var file_management_proto_rawDesc = []byte{ 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0xae, 0x02, 0x0a, 0x0e, + 0x0c, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0xaa, 0x02, 0x0a, 0x0e, 0x50, 0x65, 0x65, 0x72, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x67, 0x6f, 0x4f, 0x53, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, - 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, - 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, - 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, - 0x69, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, - 0x0a, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x4f, 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, - 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, - 0x0a, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, - 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, - 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, - 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x22, 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, - 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x65, 0x73, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, - 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, - 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, - 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, - 0x75, 0x72, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, - 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, - 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x6c, 0x22, 0x98, 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x75, 0x72, 0x69, 0x12, 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x22, 0x3b, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, - 0x55, 0x44, 0x50, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, - 0x0a, 0x04, 0x48, 0x54, 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, - 0x53, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x7d, 0x0a, - 0x13, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, - 0x75, 0x73, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x01, 0x0a, - 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, - 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, - 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, - 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, - 0x22, 0xe2, 0x03, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, - 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x4f, 0x53, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x4f, 0x53, 0x12, 0x2e, 0x0a, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, + 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x12, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x69, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6b, 0x65, 0x72, + 0x6e, 0x65, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x4f, 0x53, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x4f, + 0x53, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x94, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x11, 0x77, 0x69, + 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x65, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x77, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x3e, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x52, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, - 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, - 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, - 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x40, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, - 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, - 0x6c, 0x65, 0x52, 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, + 0x79, 0x0a, 0x11, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x38, 0x0a, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x73, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x73, 0x41, 0x74, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0xa8, 0x01, 0x0a, 0x11, 0x57, 0x69, 0x72, 0x65, 0x74, 0x72, 0x75, 0x73, + 0x74, 0x65, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x75, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x05, 0x73, 0x74, 0x75, 0x6e, 0x73, 0x12, 0x35, 0x0a, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x05, 0x74, 0x75, 0x72, 0x6e, 0x73, 0x12, 0x2e, + 0x0a, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x22, 0x98, + 0x01, 0x0a, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10, 0x0a, + 0x03, 0x75, 0x72, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, + 0x3b, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x48, + 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x3b, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, + 0x00, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54, + 0x54, 0x50, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x03, 0x12, + 0x08, 0x0a, 0x04, 0x44, 0x54, 0x4c, 0x53, 0x10, 0x04, 0x22, 0x7d, 0x0a, 0x13, 0x50, 0x72, 0x6f, + 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x36, 0x0a, 0x0a, 0x68, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a, 0x68, 0x6f, + 0x73, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x81, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x64, 0x6e, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, + 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0xe2, 0x03, 0x0a, + 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, + 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, + 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3e, 0x0a, 0x0b, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0b, + 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, + 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x52, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x6f, + 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, + 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0c, 0x6f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x3e, 0x0a, + 0x0d, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0d, + 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x66, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, - 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, - 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, - 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, - 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, - 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, - 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, - 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, - 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, - 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, - 0x0a, 0x1c, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, - 0x0a, 0x15, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, - 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, - 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, - 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, - 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, - 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, - 0x0a, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, - 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, - 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, - 0x55, 0x52, 0x4c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, - 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x49, 0x44, 0x12, 0x18, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, - 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, - 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, - 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, - 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, - 0x74, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, - 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, - 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, - 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, - 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, - 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, - 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, - 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, - 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x22, 0x74, 0x0a, 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, - 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, - 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, - 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, - 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, - 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, - 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, - 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, - 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, - 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, - 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, - 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, - 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, - 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, - 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, - 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, - 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, - 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, - 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x66, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x97, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, + 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, + 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x49, 0x0a, 0x09, 0x53, + 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, + 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, + 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, + 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, + 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, + 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, + 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, + 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, + 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x1e, 0x0a, 0x1c, 0x50, 0x4b, + 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x5b, 0x0a, 0x15, 0x50, 0x4b, + 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xea, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x2e, + 0x0a, 0x12, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x24, + 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x45, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x55, 0x73, + 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, + 0x55, 0x73, 0x65, 0x49, 0x44, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x34, 0x0a, 0x15, 0x41, 0x75, + 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, + 0x69, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, + 0x12, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x52, 0x4c, 0x73, + 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x55, 0x52, 0x4c, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x05, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x18, + 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x20, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x65, + 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x16, + 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, 0x65, + 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, 0x71, + 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4e, 0x65, 0x74, 0x49, 0x44, 0x22, 0xb4, 0x01, 0x0a, + 0x09, 0x44, 0x4e, 0x53, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0d, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x12, 0x47, 0x0a, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x10, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x43, 0x75, 0x73, + 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x75, 0x73, 0x74, + 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x0b, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, + 0x6e, 0x65, 0x73, 0x22, 0x58, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5a, 0x6f, 0x6e, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x52, 0x65, 0x63, + 0x6f, 0x72, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x74, 0x0a, + 0x0c, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, + 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, + 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, + 0x61, 0x74, 0x61, 0x22, 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, + 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, + 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, + 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, + 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, + 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, + 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, + 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, + 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, + 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, + 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, + 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, - 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, - 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, + 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, - 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, - 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, - 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, - 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, + 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, + 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index a49235dfcdf..f9612ce0978 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -97,8 +97,7 @@ message PeerSystemMeta { string hostname = 1; string goOS = 2; string kernel = 3; - // core field has been deprecated in favor of the new OsVersion field for better clarity. - string core = 4 [deprecated=true]; + string core = 4; string platform = 5; string OS = 6; string wiretrusteeVersion = 7; diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 617e35d821b..407194c6dda 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -256,7 +256,6 @@ func mapError(err error) error { func extractPeerMeta(loginReq *proto.LoginRequest) nbpeer.PeerSystemMeta { osVersion := loginReq.GetMeta().GetOSVersion() if osVersion == "" { - //nolint:staticcheck osVersion = loginReq.GetMeta().GetCore() } From 4bcee77dd2d1fa504d24ba24b7e0c674f52b0175 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 6 Feb 2024 14:47:50 +0100 Subject: [PATCH 18/35] Extend peer meta with location information (#1517) This PR uses the geolocation service to resolve IP to location. The lookup happens once on the first connection - when a client calls the Sync func. The location is stored as part of the peer: --- management/cmd/management.go | 2 +- management/server/account.go | 2 +- management/server/account_test.go | 6 +- management/server/file_store.go | 21 ++++++ management/server/file_store_test.go | 49 ++++++++++++++ management/server/geolocation/geolocation.go | 9 +-- .../server/geolocation/geolocation_test.go | 6 +- management/server/grpcserver.go | 17 ++--- management/server/http/api/openapi.yml | 12 ++++ management/server/http/api/types.gen.go | 27 ++++++++ management/server/http/peers_handler.go | 17 +++++ management/server/mock_server/account_mock.go | 7 +- management/server/peer.go | 20 +++++- management/server/peer/peer.go | 17 +++-- management/server/posture/geo_location.go | 4 +- .../server/posture/geo_location_test.go | 64 +++++++------------ management/server/sqlite_store.go | 12 ++++ management/server/sqlite_store_test.go | 43 +++++++++++++ management/server/store.go | 1 + 19 files changed, 260 insertions(+), 76 deletions(-) diff --git a/management/cmd/management.go b/management/cmd/management.go index dcafe8d5ef9..19893df5ea3 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -179,7 +179,7 @@ var ( trustedPeers := config.TrustedHTTPProxies if len(trustedPeers) == 0 { - trustedPeers = []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")} + trustedPeers = []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")} } headers := []string{realip.XForwardedFor, realip.XRealIp} gRPCOpts := []grpc.ServerOption{ diff --git a/management/server/account.go b/management/server/account.go index 8ecc2d439e6..6472752dc77 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -76,7 +76,7 @@ type AccountManager interface { GetUser(claims jwtclaims.AuthorizationClaims) (*User, error) ListUsers(accountID string) ([]*User, error) GetPeers(accountID, userID string) ([]*nbpeer.Peer, error) - MarkPeerConnected(peerKey string, connected bool) error + MarkPeerConnected(peerKey string, connected bool, realIP net.IP) error DeletePeer(accountID, peerID, userID string) error UpdatePeer(accountID, userID string, peer *nbpeer.Peer) (*nbpeer.Peer, error) GetNetworkMap(peerID string) (*NetworkMap, error) diff --git a/management/server/account_test.go b/management/server/account_test.go index f656f732301..2d144df72ba 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1638,7 +1638,7 @@ func TestDefaultAccountManager_UpdatePeer_PeerLoginExpiration(t *testing.T) { LoginExpirationEnabled: true, }) require.NoError(t, err, "unable to add peer") - err = manager.MarkPeerConnected(key.PublicKey().String(), true) + err = manager.MarkPeerConnected(key.PublicKey().String(), true, nil) require.NoError(t, err, "unable to mark peer connected") account, err = manager.UpdateAccountSettings(account.Id, userID, &Settings{ PeerLoginExpiration: time.Hour, @@ -1705,7 +1705,7 @@ func TestDefaultAccountManager_MarkPeerConnected_PeerLoginExpiration(t *testing. } // when we mark peer as connected, the peer login expiration routine should trigger - err = manager.MarkPeerConnected(key.PublicKey().String(), true) + err = manager.MarkPeerConnected(key.PublicKey().String(), true, nil) require.NoError(t, err, "unable to mark peer connected") failed := waitTimeout(wg, time.Second) @@ -1728,7 +1728,7 @@ func TestDefaultAccountManager_UpdateAccountSettings_PeerLoginExpiration(t *test LoginExpirationEnabled: true, }) require.NoError(t, err, "unable to add peer") - err = manager.MarkPeerConnected(key.PublicKey().String(), true) + err = manager.MarkPeerConnected(key.PublicKey().String(), true, nil) require.NoError(t, err, "unable to mark peer connected") wg := &sync.WaitGroup{} diff --git a/management/server/file_store.go b/management/server/file_store.go index 818d9a4db0c..f20e51a6864 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -626,6 +626,27 @@ func (s *FileStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer.P return nil } +// SavePeerLocation stores the PeerStatus in memory. It doesn't attempt to persist data to speed up things. +// Peer.Location will be saved eventually when some other changes occur. +func (s *FileStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.Peer) error { + s.mux.Lock() + defer s.mux.Unlock() + + account, err := s.getAccount(accountID) + if err != nil { + return err + } + + peer := account.Peers[peerWithLocation.ID] + if peer == nil { + return status.Errorf(status.NotFound, "peer %s not found", peerWithLocation.ID) + } + + peer.Location = peerWithLocation.Location + + return nil +} + // SaveUserLastLogin stores the last login time for a user in memory. It doesn't attempt to persist data to speed up things. func (s *FileStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error { s.mux.Lock() diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index ef97993786e..e6a721aa281 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -634,6 +634,55 @@ func TestFileStore_SavePeerStatus(t *testing.T) { assert.Equal(t, newStatus, *actual) } +func TestFileStore_SavePeerLocation(t *testing.T) { + storeDir := t.TempDir() + + err := util.CopyFileContents("testdata/store.json", filepath.Join(storeDir, "store.json")) + if err != nil { + t.Fatal(err) + } + + store, err := NewFileStore(storeDir, nil) + if err != nil { + return + } + account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) + + peer := &nbpeer.Peer{ + AccountID: account.Id, + ID: "testpeer", + Location: nbpeer.Location{ + ConnectionIP: net.ParseIP("10.0.0.0"), + CountryCode: "YY", + CityName: "City", + GeoNameID: 1, + }, + Meta: nbpeer.PeerSystemMeta{}, + } + // error is expected as peer is not in store yet + err = store.SavePeerLocation(account.Id, peer) + assert.Error(t, err) + + account.Peers[peer.ID] = peer + err = store.SaveAccount(account) + require.NoError(t, err) + + peer.Location.ConnectionIP = net.ParseIP("35.1.1.1") + peer.Location.CountryCode = "DE" + peer.Location.CityName = "Berlin" + peer.Location.GeoNameID = 2950159 + + err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) + assert.NoError(t, err) + + account, err = store.GetAccount(account.Id) + require.NoError(t, err) + + actual := account.Peers[peer.ID].Location + assert.Equal(t, peer.Location, actual) +} + func newStore(t *testing.T) *FileStore { t.Helper() store, err := NewFileStore(t.TempDir(), nil) diff --git a/management/server/geolocation/geolocation.go b/management/server/geolocation/geolocation.go index 4461bc1865f..77226e11a5c 100644 --- a/management/server/geolocation/geolocation.go +++ b/management/server/geolocation/geolocation.go @@ -102,17 +102,12 @@ func getSha256sum(mmdbPath string) ([]byte, error) { return h.Sum(nil), nil } -func (gl *Geolocation) Lookup(ip string) (*Record, error) { +func (gl *Geolocation) Lookup(ip net.IP) (*Record, error) { gl.mux.RLock() defer gl.mux.RUnlock() - parsedIp := net.ParseIP(ip) - if parsedIp == nil { - return nil, fmt.Errorf("could not parse IP %s", ip) - } - var record Record - err := gl.db.Lookup(parsedIp, &record) + err := gl.db.Lookup(ip, &record) if err != nil { return nil, err } diff --git a/management/server/geolocation/geolocation_test.go b/management/server/geolocation/geolocation_test.go index 8d4c71599be..a6b258dbf25 100644 --- a/management/server/geolocation/geolocation_test.go +++ b/management/server/geolocation/geolocation_test.go @@ -1,6 +1,7 @@ package geolocation import ( + "net" "os" "path" "testing" @@ -34,7 +35,7 @@ func TestGeoLite_Lookup(t *testing.T) { } }() - record, err := geo.Lookup("89.160.20.128") + record, err := geo.Lookup(net.ParseIP("89.160.20.128")) assert.NoError(t, err) assert.NotNil(t, record) assert.Equal(t, "SE", record.Country.ISOCode) @@ -43,7 +44,4 @@ func TestGeoLite_Lookup(t *testing.T) { assert.Equal(t, uint(2694762), record.City.GeonameID) assert.Equal(t, "EU", record.Continent.Code) assert.Equal(t, uint(6255148), record.Continent.GeonameID) - - _, err = geo.Lookup("589.160.20.128") - assert.Error(t, err) } diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 407194c6dda..22454b881d6 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "net" "strings" "time" @@ -109,11 +110,11 @@ func (s *GRPCServer) GetServerKey(ctx context.Context, req *proto.Empty) (*proto }, nil } -func getRealIP(ctx context.Context) string { - if ip, ok := realip.FromContext(ctx); ok { - return ip.String() +func getRealIP(ctx context.Context) net.IP { + if addr, ok := realip.FromContext(ctx); ok { + return net.IP(addr.AsSlice()) } - return "" + return nil } // Sync validates the existence of a connecting peer, sends an initial state (all available for the connecting peers) and @@ -124,7 +125,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi s.appMetrics.GRPCMetrics().CountSyncRequest() } realIP := getRealIP(srv.Context()) - log.Debugf("Sync request from peer [%s] [%s]", req.WgPubKey, realIP) + log.Debugf("Sync request from peer [%s] [%s]", req.WgPubKey, realIP.String()) syncReq := &proto.SyncRequest{} peerKey, err := s.parseRequest(req, syncReq) @@ -147,7 +148,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi s.ephemeralManager.OnPeerConnected(peer) - err = s.accountManager.MarkPeerConnected(peerKey.String(), true) + err = s.accountManager.MarkPeerConnected(peerKey.String(), true, realIP) if err != nil { log.Warnf("failed marking peer as connected %s %v", peerKey, err) } @@ -205,7 +206,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi func (s *GRPCServer) cancelPeerRoutines(peer *nbpeer.Peer) { s.peersUpdateManager.CloseChannel(peer.ID) s.turnCredentialsManager.CancelRefresh(peer.ID) - _ = s.accountManager.MarkPeerConnected(peer.Key, false) + _ = s.accountManager.MarkPeerConnected(peer.Key, false, nil) s.ephemeralManager.OnPeerDisconnected(peer) } @@ -302,7 +303,7 @@ func (s *GRPCServer) Login(ctx context.Context, req *proto.EncryptedMessage) (*p s.appMetrics.GRPCMetrics().CountLoginRequest() } realIP := getRealIP(ctx) - log.Debugf("Login request from peer [%s] [%s]", req.WgPubKey, realIP) + log.Debugf("Login request from peer [%s] [%s]", req.WgPubKey, realIP.String()) loginReq := &proto.LoginRequest{} peerKey, err := s.parseRequest(req, loginReq) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index e35d12ce106..596a926840c 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -247,6 +247,10 @@ components: description: Peer's IP address type: string example: 10.64.0.1 + connection_ip: + description: Peer's public connection IP address + type: string + example: 35.64.0.1 connected: description: Peer to Management connection status type: boolean @@ -260,6 +264,14 @@ components: description: Peer's operating system and version type: string example: Darwin 13.2.1 + kernel_version: + description: Peer's operating system kernel version + type: string + example: 23.2.0 + geoname_id: + description: Unique identifier from the GeoNames database for a specific geographical location. + type: integer + example: 2643743 version: description: Peer's daemon or cli version type: string diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index 34d4ef3ed9e..2f0bb453305 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -412,9 +412,15 @@ type Peer struct { // Connected Peer to Management connection status Connected bool `json:"connected"` + // ConnectionIp Peer's public connection IP address + ConnectionIp *string `json:"connection_ip,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // GeonameId Unique identifier from the GeoNames database for a specific geographical location. + GeonameId *int `json:"geoname_id,omitempty"` + // Groups Groups that the peer belongs to Groups []GroupMinimum `json:"groups"` @@ -427,6 +433,9 @@ type Peer struct { // Ip Peer's IP address Ip string `json:"ip"` + // KernelVersion Peer's operating system kernel version + KernelVersion *string `json:"kernel_version,omitempty"` + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. LastLogin time.Time `json:"last_login"` @@ -466,9 +475,15 @@ type PeerBase struct { // Connected Peer to Management connection status Connected bool `json:"connected"` + // ConnectionIp Peer's public connection IP address + ConnectionIp *string `json:"connection_ip,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // GeonameId Unique identifier from the GeoNames database for a specific geographical location. + GeonameId *int `json:"geoname_id,omitempty"` + // Groups Groups that the peer belongs to Groups []GroupMinimum `json:"groups"` @@ -481,6 +496,9 @@ type PeerBase struct { // Ip Peer's IP address Ip string `json:"ip"` + // KernelVersion Peer's operating system kernel version + KernelVersion *string `json:"kernel_version,omitempty"` + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. LastLogin time.Time `json:"last_login"` @@ -523,9 +541,15 @@ type PeerBatch struct { // Connected Peer to Management connection status Connected bool `json:"connected"` + // ConnectionIp Peer's public connection IP address + ConnectionIp *string `json:"connection_ip,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` + // GeonameId Unique identifier from the GeoNames database for a specific geographical location. + GeonameId *int `json:"geoname_id,omitempty"` + // Groups Groups that the peer belongs to Groups []GroupMinimum `json:"groups"` @@ -538,6 +562,9 @@ type PeerBatch struct { // Ip Peer's IP address Ip string `json:"ip"` + // KernelVersion Peer's operating system kernel version + KernelVersion *string `json:"kernel_version,omitempty"` + // LastLogin Last time this peer performed log in (authentication). E.g., user authenticated. LastLogin time.Time `json:"last_login"` diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index 2878136df63..66ecc4bf7d5 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -3,6 +3,7 @@ package http import ( "encoding/json" "fmt" + "net" "net/http" "github.com/gorilla/mux" @@ -230,18 +231,30 @@ func toGroupsInfo(groups map[string]*server.Group, peerID string) []api.GroupMin return groupsInfo } +func connectionIPoString(ip net.IP) *string { + publicIP := "" + if ip != nil { + publicIP = ip.String() + } + return &publicIP +} + func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsDomain string, accessiblePeer []api.AccessiblePeer) *api.Peer { osVersion := peer.Meta.OSVersion if osVersion == "" { osVersion = peer.Meta.Core } + geonameID := int(peer.Location.GeoNameID) return &api.Peer{ Id: peer.ID, Name: peer.Name, Ip: peer.IP.String(), + ConnectionIp: connectionIPoString(peer.Location.ConnectionIP), Connected: peer.Status.Connected, LastSeen: peer.Status.LastSeen, Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), + KernelVersion: &peer.Meta.KernelVersion, + GeonameId: &geonameID, Version: peer.Meta.WtVersion, Groups: groupsInfo, SshEnabled: peer.SSHEnabled, @@ -262,13 +275,17 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn if osVersion == "" { osVersion = peer.Meta.Core } + geonameID := int(peer.Location.GeoNameID) return &api.PeerBatch{ Id: peer.ID, Name: peer.Name, Ip: peer.IP.String(), + ConnectionIp: connectionIPoString(peer.Location.ConnectionIP), Connected: peer.Status.Connected, LastSeen: peer.Status.LastSeen, Os: fmt.Sprintf("%s %s", peer.Meta.OS, osVersion), + KernelVersion: &peer.Meta.KernelVersion, + GeonameId: &geonameID, Version: peer.Meta.WtVersion, Groups: groupsInfo, SshEnabled: peer.SSHEnabled, diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index fbd986dcec5..9dd0810c7b1 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -1,6 +1,7 @@ package mock_server import ( + "net" "time" "google.golang.org/grpc/codes" @@ -24,7 +25,7 @@ type MockAccountManager struct { GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error) ListUsersFunc func(accountID string) ([]*server.User, error) GetPeersFunc func(accountID, userID string) ([]*nbpeer.Peer, error) - MarkPeerConnectedFunc func(peerKey string, connected bool) error + MarkPeerConnectedFunc func(peerKey string, connected bool, realIP net.IP) error DeletePeerFunc func(accountID, peerKey, userID string) error GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error) GetPeerNetworkFunc func(peerKey string) (*server.Network, error) @@ -152,9 +153,9 @@ func (am *MockAccountManager) GetAccountByUserOrAccountID( } // MarkPeerConnected mock implementation of MarkPeerConnected from server.AccountManager interface -func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) error { +func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool, realIP net.IP) error { if am.MarkPeerConnectedFunc != nil { - return am.MarkPeerConnectedFunc(peerKey, connected) + return am.MarkPeerConnectedFunc(peerKey, connected, realIP) } return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented") } diff --git a/management/server/peer.go b/management/server/peer.go index 2b3bfe16774..1a7b06a793a 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -2,6 +2,7 @@ package server import ( "fmt" + "net" "strings" "time" @@ -80,7 +81,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.P } // MarkPeerConnected marks peer as connected (true) or disconnected (false) -func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected bool) error { +func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected bool, realIP net.IP) error { account, err := am.Store.GetAccountByPeerPubKey(peerPubKey) if err != nil { return err @@ -109,6 +110,23 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected newStatus.LoginExpired = false } peer.Status = newStatus + + if am.geo != nil && realIP != nil { + location, err := am.geo.Lookup(realIP) + if err != nil { + log.Warnf("failed to get location for peer %s realip: [%s]: %v", peer.ID, realIP.String(), err) + } else { + peer.Location.ConnectionIP = realIP + peer.Location.CountryCode = location.Country.ISOCode + peer.Location.CityName = location.City.Names.En + peer.Location.GeoNameID = location.City.GeonameID + err = am.Store.SavePeerLocation(account.Id, peer) + if err != nil { + log.Warnf("could not store location for peer %s: %s", peer.ID, err) + } + } + } + account.UpdatePeer(peer) err = am.Store.SavePeerStatus(account.Id, peer.ID, *newStatus) diff --git a/management/server/peer/peer.go b/management/server/peer/peer.go index 6ed2c793853..4ed6c458dfe 100644 --- a/management/server/peer/peer.go +++ b/management/server/peer/peer.go @@ -41,6 +41,8 @@ type Peer struct { LastLogin time.Time // Indicate ephemeral peer attribute Ephemeral bool + // Geo location based on connection IP + Location Location `gorm:"embedded;embeddedPrefix:location_"` } type PeerStatus struct { @@ -54,6 +56,14 @@ type PeerStatus struct { RequiresApproval bool } +// Location is a geo location information of a Peer based on public connection IP +type Location struct { + ConnectionIP net.IP // from grpc peer or reverse proxy headers depends on setup + CountryCode string + CityName string + GeoNameID uint // city level geoname id +} + // PeerSystemMeta is a metadata of a Peer machine system type PeerSystemMeta struct { Hostname string @@ -66,12 +76,6 @@ type PeerSystemMeta struct { WtVersion string UIVersion string KernelVersion string - // Location mock location for peer - // TODO: Add actual implementation based on peer IP - Location struct { - CountryCode string - CityName string - } `gorm:"embedded;embeddedPrefix:location_"` } func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool { @@ -114,6 +118,7 @@ func (p *Peer) Copy() *Peer { LoginExpirationEnabled: p.LoginExpirationEnabled, LastLogin: p.LastLogin, Ephemeral: p.Ephemeral, + Location: p.Location, } } diff --git a/management/server/posture/geo_location.go b/management/server/posture/geo_location.go index 303c1f1fd25..72ff3e6daff 100644 --- a/management/server/posture/geo_location.go +++ b/management/server/posture/geo_location.go @@ -31,8 +31,8 @@ type GeoLocationCheck struct { func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) { for _, loc := range g.Locations { - if loc.CountryCode == peer.Meta.Location.CountryCode { - if loc.CityName == "" || loc.CityName == peer.Meta.Location.CityName { + if loc.CountryCode == peer.Location.CountryCode { + if loc.CityName == "" || loc.CityName == peer.Location.CityName { switch g.Action { case GeoLocationActionDeny: return false, nil diff --git a/management/server/posture/geo_location_test.go b/management/server/posture/geo_location_test.go index a2345f09ef0..908e42eb958 100644 --- a/management/server/posture/geo_location_test.go +++ b/management/server/posture/geo_location_test.go @@ -19,11 +19,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location matches the location in the allow sets", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Berlin", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Berlin", }, }, check: GeoLocationCheck{ @@ -45,11 +43,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location matches the location in the allow country only", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Berlin", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Berlin", }, }, check: GeoLocationCheck{ @@ -66,11 +62,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location doesn't match the location in the allow sets", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Frankfurt am Main", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", }, }, check: GeoLocationCheck{ @@ -92,11 +86,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location doesn't match the location in the allow country only", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Frankfurt am Main", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", }, }, check: GeoLocationCheck{ @@ -113,11 +105,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location matches the location in the deny sets", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Berlin", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Berlin", }, }, check: GeoLocationCheck{ @@ -139,11 +129,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location matches the location in the deny country only", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Berlin", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Berlin", }, }, check: GeoLocationCheck{ @@ -163,11 +151,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location doesn't match the location in the deny sets", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Frankfurt am Main", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", }, }, check: GeoLocationCheck{ @@ -189,11 +175,9 @@ func TestGeoLocationCheck_Check(t *testing.T) { { name: "Peer location doesn't match the location in the deny country only", input: peer.Peer{ - Meta: peer.PeerSystemMeta{ - Location: Location{ - CountryCode: "DE", - CityName: "Frankfurt am Main", - }, + Location: peer.Location{ + CountryCode: "DE", + CityName: "Frankfurt am Main", }, }, check: GeoLocationCheck{ diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 578a148327a..5788e52d45d 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -267,6 +267,18 @@ func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus nbpeer return s.db.Save(peer).Error } +func (s *SqliteStore) SavePeerLocation(accountID string, peerWithLocation *nbpeer.Peer) error { + var peer nbpeer.Peer + result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerWithLocation.ID) + if result.Error != nil { + return status.Errorf(status.NotFound, "peer %s not found", peer.ID) + } + + peer.Location = peerWithLocation.Location + + return s.db.Save(peer).Error +} + // DeleteHashedPAT2TokenIDIndex is noop in Sqlite func (s *SqliteStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error { return nil diff --git a/management/server/sqlite_store_test.go b/management/server/sqlite_store_test.go index e493368fafe..29b49d7f3b1 100644 --- a/management/server/sqlite_store_test.go +++ b/management/server/sqlite_store_test.go @@ -212,6 +212,49 @@ func TestSqlite_SavePeerStatus(t *testing.T) { actual := account.Peers["testpeer"].Status assert.Equal(t, newStatus, *actual) } +func TestSqlite_SavePeerLocation(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStoreFromFile(t, "testdata/store.json") + + account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) + + peer := &nbpeer.Peer{ + AccountID: account.Id, + ID: "testpeer", + Location: nbpeer.Location{ + ConnectionIP: net.ParseIP("0.0.0.0"), + CountryCode: "YY", + CityName: "City", + GeoNameID: 1, + }, + Meta: nbpeer.PeerSystemMeta{}, + } + // error is expected as peer is not in store yet + err = store.SavePeerLocation(account.Id, peer) + assert.Error(t, err) + + account.Peers[peer.ID] = peer + err = store.SaveAccount(account) + require.NoError(t, err) + + peer.Location.ConnectionIP = net.ParseIP("35.1.1.1") + peer.Location.CountryCode = "DE" + peer.Location.CityName = "Berlin" + peer.Location.GeoNameID = 2950159 + + err = store.SavePeerLocation(account.Id, account.Peers[peer.ID]) + assert.NoError(t, err) + + account, err = store.GetAccount(account.Id) + require.NoError(t, err) + + actual := account.Peers[peer.ID].Location + assert.Equal(t, peer.Location, actual) +} func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { if runtime.GOOS == "windows" { diff --git a/management/server/store.go b/management/server/store.go index a482ca9470c..3a96c32401b 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -33,6 +33,7 @@ type Store interface { // AcquireGlobalLock should attempt to acquire a global lock and return a function that releases the lock AcquireGlobalLock() func() SavePeerStatus(accountID, peerID string, status nbpeer.PeerStatus) error + SavePeerLocation(accountID string, peer *nbpeer.Peer) error SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error // Close should close the store persisting all unsaved data. Close() error From 6b11bf0e527ece540fc0bd546b040ca5ea2bc2ba Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 7 Feb 2024 18:40:53 +0300 Subject: [PATCH 19/35] Add Locations endpoints (#1516) * add locations endpoints * Add sqlite3 check and database generation in geolite script * Add SQLite storage for geolocation data * Refactor file existence check into a separate function * Integrate geolocation services into management application * Refactoring * Refactor city retrieval to include Geonames ID * Add signature verification for GeoLite2 database download * Change to in-memory database for geolocation store * Merge manager to geolocation * Update GetAllCountries to return Country name and iso code * fix tests * Add reload to SqliteStore * Add geoname indexes * move db file check to connectDB * Add concurrency safety to SQL queries and database reloading The commit adds mutex locks to the GetAllCountries and GetCitiesByCountry functions to ensure thread-safety during database queries. Additionally, it introduces a mechanism to safely close the old database connection before a new connection is established upon reloading, which improves the reliability of database operations. Lastly, it moves the checking of database file existence to the connectDB function. * Add sha256 sum check to geolocation store before reload * Use read lock * Check SHA256 twice when reload geonames db --------- Co-authored-by: Yury Gargay --- infrastructure_files/download-geolite2.sh | 124 ++++++++--- management/cmd/management.go | 3 +- management/server/geolocation/geolocation.go | 66 +++++- .../server/geolocation/geolocation_test.go | 12 +- management/server/geolocation/store.go | 208 ++++++++++++++++++ management/server/http/api/openapi.yml | 85 +++++++ management/server/http/api/types.gen.go | 18 ++ .../server/http/geolocations_handler.go | 107 +++++++++ management/server/http/handler.go | 27 ++- 9 files changed, 604 insertions(+), 46 deletions(-) create mode 100644 management/server/geolocation/store.go create mode 100644 management/server/http/geolocations_handler.go diff --git a/infrastructure_files/download-geolite2.sh b/infrastructure_files/download-geolite2.sh index 2bd1c09045e..7cc5528ae29 100755 --- a/infrastructure_files/download-geolite2.sh +++ b/infrastructure_files/download-geolite2.sh @@ -22,41 +22,95 @@ then exit 1 fi -DATABASE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz" -SIGNATURE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" - -# Download the database and signature files -echo "Downloading database file..." -DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") -echo "Downloading signature file..." -SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") - -# Verify the signature -echo "Verifying signature..." -if sha256sum -c --status "$SIGNATURE_FILE"; then - echo "Signature is valid." -else - echo "Signature is invalid. Aborting." +if ! command -v sqlite3 &> /dev/null +then + echo "sqlite3 is not installed or not in PATH, please install with your package manager. e.g. sudo apt install sqlite3" > /dev/stderr exit 1 fi -# Unpack the database file -EXTRACTION_DIR=$(basename "$DATABASE_FILE" .tar.gz) -echo "Unpacking $DATABASE_FILE..." -mkdir -p "$EXTRACTION_DIR" -tar -xzvf "$DATABASE_FILE" - -# Create a SHA256 signature file -MMDB_FILE="GeoLite2-City.mmdb" -cd "$EXTRACTION_DIR" -sha256sum "$MMDB_FILE" > "$MMDB_FILE.sha256" -echo "SHA256 signature created for $MMDB_FILE." -cd - - -# Remove downloaded files -rm "$DATABASE_FILE" "$SIGNATURE_FILE" - -# Done. Print next steps -echo "Process completed successfully." -echo "Now you can place $EXTRACTION_DIR/$MMDB_FILE to 'datadir' of management service." -echo -e "Example:\n\tdocker compose cp $EXTRACTION_DIR/$MMDB_FILE management:/var/lib/netbird/" +download_geolite_mmdb() { + DATABASE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz" + SIGNATURE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" + + # Download the database and signature files + echo "Downloading database file..." + DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") + echo "Downloading signature file..." + SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") + + # Verify the signature + echo "Verifying signature..." + if sha256sum -c --status "$SIGNATURE_FILE"; then + echo "Signature is valid." + else + echo "Signature is invalid. Aborting." + exit 1 + fi + + # Unpack the database file + EXTRACTION_DIR=$(basename "$DATABASE_FILE" .tar.gz) + echo "Unpacking $DATABASE_FILE..." + mkdir -p "$EXTRACTION_DIR" + tar -xzvf "$DATABASE_FILE" + + # Create a SHA256 signature file + MMDB_FILE="GeoLite2-City.mmdb" + cd "$EXTRACTION_DIR" + sha256sum "$MMDB_FILE" > "$MMDB_FILE.sha256" + echo "SHA256 signature created for $MMDB_FILE." + cd - + + # Remove downloaded files + rm "$DATABASE_FILE" "$SIGNATURE_FILE" + + # Done. Print next steps + echo "Process completed successfully." + echo "Now you can place $EXTRACTION_DIR/$MMDB_FILE to 'datadir' of management service." + echo -e "Example:\n\tdocker compose cp $EXTRACTION_DIR/$MMDB_FILE management:/var/lib/netbird/" +} + + +download_geolite_csv_and_create_sqlite_db() { + DATABASE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City-CSV/download?suffix=zip" + SIGNATURE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City-CSV/download?suffix=zip.sha256" + + + # Download the database file + echo "Downloading database file..." + DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") + echo "Downloading signature file..." + SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") + + # Verify the signature + echo "Verifying signature..." + if sha256sum -c --status "$SIGNATURE_FILE"; then + echo "Signature is valid." + else + echo "Signature is invalid. Aborting." + exit 1 + fi + + # Unpack the database file + EXTRACTION_DIR=$(basename "$DATABASE_FILE" .zip) + DB_NAME="geonames.db" + + unzip "$DATABASE_FILE" + +# Create SQLite database and import data from CSV +sqlite3 "$DB_NAME" < Date: Thu, 8 Feb 2024 15:44:52 +0300 Subject: [PATCH 20/35] Add tests and validation for empty peer location in GeoLocationCheck (#1546) --- management/server/posture/geo_location.go | 5 ++++ .../server/posture/geo_location_test.go | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/management/server/posture/geo_location.go b/management/server/posture/geo_location.go index 72ff3e6daff..ceade0d2ae5 100644 --- a/management/server/posture/geo_location.go +++ b/management/server/posture/geo_location.go @@ -30,6 +30,11 @@ type GeoLocationCheck struct { } func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) { + // deny if the peer location is not evaluated + if peer.Location.CountryCode == "" && peer.Location.CityName == "" { + return false, fmt.Errorf("peer's location is not set") + } + for _, loc := range g.Locations { if loc.CountryCode == peer.Location.CountryCode { if loc.CityName == "" || loc.CityName == peer.Location.CityName { diff --git a/management/server/posture/geo_location_test.go b/management/server/posture/geo_location_test.go index 908e42eb958..7a886a2827e 100644 --- a/management/server/posture/geo_location_test.go +++ b/management/server/posture/geo_location_test.go @@ -192,6 +192,36 @@ func TestGeoLocationCheck_Check(t *testing.T) { wantErr: false, isValid: true, }, + { + name: "Peer with no location in the allow sets", + input: peer.Peer{}, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + Action: GeoLocationActionAllow, + }, + wantErr: true, + isValid: false, + }, + { + name: "Peer with no location in the deny sets", + input: peer.Peer{}, + check: GeoLocationCheck{ + Locations: []Location{ + { + CountryCode: "DE", + CityName: "Berlin", + }, + }, + Action: GeoLocationActionDeny, + }, + wantErr: true, + isValid: false, + }, } for _, tt := range tests { From b284c4d1c6005676e6589a960346f504d79da678 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 8 Feb 2024 18:24:57 +0300 Subject: [PATCH 21/35] Disallow Geo check creation/update without configured Geo DB (#1548) --- .../server/http/geolocations_handler.go | 4 +- management/server/http/handler.go | 4 +- .../server/http/posture_checks_handler.go | 15 ++- .../http/posture_checks_handler_test.go | 126 +++++++++++++++++- 4 files changed, 139 insertions(+), 10 deletions(-) diff --git a/management/server/http/geolocations_handler.go b/management/server/http/geolocations_handler.go index 45d14460ff3..30958968774 100644 --- a/management/server/http/geolocations_handler.go +++ b/management/server/http/geolocations_handler.go @@ -20,8 +20,8 @@ type GeolocationsHandler struct { claimsExtractor *jwtclaims.ClaimsExtractor } -// NewLocationsHandlerHandler creates a new Location handler -func NewLocationsHandlerHandler(accountManager server.AccountManager, geolocationManager *geolocation.Geolocation, authCfg AuthCfg) *GeolocationsHandler { +// NewGeolocationsHandlerHandler creates a new Geolocations handler +func NewGeolocationsHandlerHandler(accountManager server.AccountManager, geolocationManager *geolocation.Geolocation, authCfg AuthCfg) *GeolocationsHandler { return &GeolocationsHandler{ accountManager: accountManager, geolocationManager: geolocationManager, diff --git a/management/server/http/handler.go b/management/server/http/handler.go index fd0a4eb21c2..168b05571f0 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -207,7 +207,7 @@ func (apiHandler *apiHandler) addEventsEndpoint() { } func (apiHandler *apiHandler) addPostureCheckEndpoint() { - postureCheckHandler := NewPostureChecksHandler(apiHandler.AccountManager, apiHandler.AuthCfg) + postureCheckHandler := NewPostureChecksHandler(apiHandler.AccountManager, apiHandler.geolocationManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.GetAllPostureChecks).Methods("GET", "OPTIONS") apiHandler.Router.HandleFunc("/posture-checks", postureCheckHandler.CreatePostureCheck).Methods("POST", "OPTIONS") apiHandler.Router.HandleFunc("/posture-checks/{postureCheckId}", postureCheckHandler.UpdatePostureCheck).Methods("PUT", "OPTIONS") @@ -218,7 +218,7 @@ func (apiHandler *apiHandler) addPostureCheckEndpoint() { func (apiHandler *apiHandler) addLocationsEndpoint() { // enable location endpoints if location manager is enabled if apiHandler.geolocationManager != nil { - locationHandler := NewLocationsHandlerHandler(apiHandler.AccountManager, apiHandler.geolocationManager, apiHandler.AuthCfg) + locationHandler := NewGeolocationsHandlerHandler(apiHandler.AccountManager, apiHandler.geolocationManager, apiHandler.AuthCfg) apiHandler.Router.HandleFunc("/locations/countries", locationHandler.GetAllCountries).Methods("GET", "OPTIONS") apiHandler.Router.HandleFunc("/locations/countries/{country}/cities", locationHandler.GetCitiesByCountry).Methods("GET", "OPTIONS") } diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index d39fcbe7cbb..bc50cfed7e1 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -9,6 +9,7 @@ import ( "github.com/rs/xid" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/http/util" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -22,14 +23,16 @@ var ( // PostureChecksHandler is a handler that returns posture checks of the account. type PostureChecksHandler struct { - accountManager server.AccountManager - claimsExtractor *jwtclaims.ClaimsExtractor + accountManager server.AccountManager + geolocationManager *geolocation.Geolocation + claimsExtractor *jwtclaims.ClaimsExtractor } // NewPostureChecksHandler creates a new PostureChecks handler -func NewPostureChecksHandler(accountManager server.AccountManager, authCfg AuthCfg) *PostureChecksHandler { +func NewPostureChecksHandler(accountManager server.AccountManager, geolocationManager *geolocation.Geolocation, authCfg AuthCfg) *PostureChecksHandler { return &PostureChecksHandler{ - accountManager: accountManager, + accountManager: accountManager, + geolocationManager: geolocationManager, claimsExtractor: jwtclaims.NewClaimsExtractor( jwtclaims.WithAudience(authCfg.Audience), jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), @@ -201,6 +204,10 @@ func (p *PostureChecksHandler) savePostureChecks( } if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { + if p.geolocationManager == nil { + util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) + return + } postureChecks.Checks = append(postureChecks.Checks, toPostureGeoLocationCheck(geoLocationCheck)) } diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index 857950fe0ca..195812cc135 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/mock_server" @@ -67,6 +68,7 @@ func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksH }, user, nil }, }, + geolocationManager: &geolocation.Geolocation{}, claimsExtractor: jwtclaims.NewClaimsExtractor( jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { return jwtclaims.AuthorizationClaims{ @@ -208,6 +210,7 @@ func TestPostureCheckUpdate(t *testing.T) { requestType string requestPath string requestBody io.Reader + setupHandlerFunc func(handler *PostureChecksHandler) }{ { name: "Create Posture Checks NB version", @@ -236,6 +239,36 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Create Posture Checks NB version with No geolocation DB", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "nb_version_check": { + "min_version": "1.2.3" + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str("default"), + Checks: api.Checks{ + NbVersionCheck: &api.NBVersionCheck{ + MinVersion: "1.2.3", + }, + }, + }, + setupHandlerFunc: func(handler *PostureChecksHandler) { + handler.geolocationManager = nil + }, + }, { name: "Create Posture Checks OS version", requestType: http.MethodPost, @@ -318,6 +351,32 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Create Posture Checks Geo Location with No geolocation DB", + requestType: http.MethodPost, + requestPath: "/api/posture-checks", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "description": "default", + "checks": { + "geo_location_check": { + "locations": [ + { + "city_name": "Berlin", + "country_code": "DE" + } + ], + "action": "allow" + } + } + }`)), + expectedStatus: http.StatusPreconditionFailed, + expectedBody: false, + setupHandlerFunc: func(handler *PostureChecksHandler) { + handler.geolocationManager = nil + }, + }, { name: "Create Posture Checks Invalid Check", requestType: http.MethodPost, @@ -433,6 +492,39 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Update Posture Checks OS Version with No geolocation DB", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/osPostureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "os_version_check": { + "linux": { + "min_kernel_version": "6.9.0" + } + } + } + }`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedPostureCheck: &api.PostureCheck{ + Id: "postureCheck", + Name: "default", + Description: str(""), + Checks: api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Linux: &api.MinKernelVersionCheck{ + MinKernelVersion: "6.9.0", + }, + }, + }, + }, + setupHandlerFunc: func(handler *PostureChecksHandler) { + handler.geolocationManager = nil + }, + }, { name: "Update Posture Checks Geo Location", requestType: http.MethodPut, @@ -471,6 +563,31 @@ func TestPostureCheckUpdate(t *testing.T) { }, }, }, + { + name: "Update Posture Checks Geo Location with No geolocation DB", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/geoPostureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "geo_location_check": { + "locations": [ + { + "city_name": "Los Angeles", + "country_code": "US" + } + ], + "action": "allow" + } + } + }`)), + expectedStatus: http.StatusPreconditionFailed, + expectedBody: false, + setupHandlerFunc: func(handler *PostureChecksHandler) { + handler.geolocationManager = nil + }, + }, { name: "Update Posture Checks Invalid Check", requestType: http.MethodPut, @@ -560,9 +677,14 @@ func TestPostureCheckUpdate(t *testing.T) { recorder := httptest.NewRecorder() req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) + defaultHandler := *p + if tc.setupHandlerFunc != nil { + tc.setupHandlerFunc(&defaultHandler) + } + router := mux.NewRouter() - router.HandleFunc("/api/posture-checks", p.CreatePostureCheck).Methods("POST") - router.HandleFunc("/api/posture-checks/{postureCheckId}", p.UpdatePostureCheck).Methods("PUT") + router.HandleFunc("/api/posture-checks", defaultHandler.CreatePostureCheck).Methods("POST") + router.HandleFunc("/api/posture-checks/{postureCheckId}", defaultHandler.UpdatePostureCheck).Methods("PUT") router.ServeHTTP(recorder, req) res := recorder.Result() From c49bf62ce0a8c578900d7d36d01211e8eaf3afd7 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Fri, 9 Feb 2024 08:24:44 +0100 Subject: [PATCH 22/35] Fix shared access to in memory copy of geonames.db (#1550) --- management/server/geolocation/geolocation.go | 4 ++-- management/server/geolocation/geolocation_test.go | 2 +- management/server/geolocation/store.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/management/server/geolocation/geolocation.go b/management/server/geolocation/geolocation.go index 69e45232ace..77108ad054a 100644 --- a/management/server/geolocation/geolocation.go +++ b/management/server/geolocation/geolocation.go @@ -19,7 +19,7 @@ const mmdbFileName = "GeoLite2-City.mmdb" type Geolocation struct { mmdbPath string - mux *sync.RWMutex + mux sync.RWMutex sha256sum []byte db *maxminddb.Reader locationDB *SqliteStore @@ -74,7 +74,7 @@ func NewGeolocation(datadir string) (*Geolocation, error) { geo := &Geolocation{ mmdbPath: mmdbPath, - mux: &sync.RWMutex{}, + mux: sync.RWMutex{}, sha256sum: sha256sum, db: db, locationDB: locationDB, diff --git a/management/server/geolocation/geolocation_test.go b/management/server/geolocation/geolocation_test.go index 0c4c35e9c56..019baed1cbe 100644 --- a/management/server/geolocation/geolocation_test.go +++ b/management/server/geolocation/geolocation_test.go @@ -31,7 +31,7 @@ func TestGeoLite_Lookup(t *testing.T) { assert.NoError(t, err) geo := &Geolocation{ - mux: &sync.RWMutex{}, + mux: sync.RWMutex{}, db: db, stopCh: make(chan struct{}), } diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 93f8eacdd5c..2dc92735961 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -22,7 +22,7 @@ const ( type SqliteStore struct { db *gorm.DB filePath string - mux *sync.RWMutex + mux sync.RWMutex sha256sum []byte } @@ -42,7 +42,7 @@ func NewSqliteStore(dataDir string) (*SqliteStore, error) { return &SqliteStore{ db: db, filePath: file, - mux: &sync.RWMutex{}, + mux: sync.RWMutex{}, sha256sum: sha256sum, }, nil } @@ -144,9 +144,9 @@ func connectDB(filePath string) (*gorm.DB, error) { return nil, err } - storeStr := ":memory:?cache=shared&mode=ro" + storeStr := "file::memory:?cache=shared" if runtime.GOOS == "windows" { - storeStr = ":memory:?&mode=ro" + storeStr = "file::memory:" } db, err := gorm.Open(sqlite.Open(storeStr), &gorm.Config{ From 7072c0273355feb665d7509d323d1995e8a973f6 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Fri, 9 Feb 2024 13:09:19 +0100 Subject: [PATCH 23/35] Trim suffix in when evaluate Min Kernel Version in OS check --- management/server/posture/os_version.go | 5 ++++- management/server/posture/os_version_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/management/server/posture/os_version.go b/management/server/posture/os_version.go index b3cae5478c0..4c311f01b94 100644 --- a/management/server/posture/os_version.go +++ b/management/server/posture/os_version.go @@ -1,6 +1,8 @@ package posture import ( + "strings" + "github.com/hashicorp/go-version" nbpeer "github.com/netbirdio/netbird/management/server/peer" log "github.com/sirupsen/logrus" @@ -34,7 +36,8 @@ func (c *OSVersionCheck) Check(peer nbpeer.Peer) (bool, error) { case "ios": return checkMinVersion(peerGoOS, peer.Meta.OSVersion, c.Ios) case "linux": - return checkMinKernelVersion(peerGoOS, peer.Meta.KernelVersion, c.Linux) + kernelVersion := strings.Split(peer.Meta.KernelVersion, "-")[0] + return checkMinKernelVersion(peerGoOS, kernelVersion, c.Linux) case "windows": return checkMinKernelVersion(peerGoOS, peer.Meta.KernelVersion, c.Windows) } diff --git a/management/server/posture/os_version_test.go b/management/server/posture/os_version_test.go index 8e1f66e67d6..03e60082e3c 100644 --- a/management/server/posture/os_version_test.go +++ b/management/server/posture/os_version_test.go @@ -32,6 +32,22 @@ func TestOSVersionCheck_Check(t *testing.T) { wantErr: false, isValid: true, }, + { + name: "Valid Peer Linux Kernel version with suffix", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + KernelVersion: "6.5.11-linuxkit", + }, + }, + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{ + MinKernelVersion: "6.0.0", + }, + }, + wantErr: false, + isValid: true, + }, { name: "Not valid Peer macOS version", input: peer.Peer{ From ddf01ac5d25f5b425242c4814de84edca4f45b29 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Fri, 9 Feb 2024 15:10:34 +0100 Subject: [PATCH 24/35] Add Valid Peer Windows Kernel version test --- management/server/posture/os_version_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/management/server/posture/os_version_test.go b/management/server/posture/os_version_test.go index 03e60082e3c..32bf5266091 100644 --- a/management/server/posture/os_version_test.go +++ b/management/server/posture/os_version_test.go @@ -16,6 +16,22 @@ func TestOSVersionCheck_Check(t *testing.T) { wantErr bool isValid bool }{ + { + name: "Valid Peer Windows Kernel version", + input: peer.Peer{ + Meta: peer.PeerSystemMeta{ + GoOS: "linux", + KernelVersion: "10.0.20348.2227", + }, + }, + check: OSVersionCheck{ + Linux: &MinKernelVersionCheck{ + MinKernelVersion: "10.0.20340.2200", + }, + }, + wantErr: false, + isValid: true, + }, { name: "Valid Peer Linux Kernel version", input: peer.Peer{ From 59480b9a84b34c4f95656938942f1aed9d7d5e20 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 12 Feb 2024 12:00:54 +0300 Subject: [PATCH 25/35] Add Geolocation handler tests (#1556) * Implement user admin checks in posture checks * Add geolocation handler tests * Mark initGeolocationTestData as helper func * Add error handling to geolocation database closure * Add cleanup function to close geolocation resources --- .gitignore | 1 - management/server/geolocation/geolocation.go | 13 +- .../server/geolocation/geolocation_test.go | 2 +- management/server/geolocation/store.go | 4 +- .../server/http/geolocation_handler_test.go | 236 ++++++++++++++++++ management/server/posture_checks.go | 18 ++ management/server/testdata/geonames-test.db | Bin 0 -> 16384 bytes 7 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 management/server/http/geolocation_handler_test.go create mode 100644 management/server/testdata/geonames-test.db diff --git a/.gitignore b/.gitignore index 1467eebacb9..cdce4697529 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,4 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store -*.db GeoLite2-City* \ No newline at end of file diff --git a/management/server/geolocation/geolocation.go b/management/server/geolocation/geolocation.go index 77108ad054a..de7a8af82b4 100644 --- a/management/server/geolocation/geolocation.go +++ b/management/server/geolocation/geolocation.go @@ -15,7 +15,7 @@ import ( log "github.com/sirupsen/logrus" ) -const mmdbFileName = "GeoLite2-City.mmdb" +const MMDBFileName = "GeoLite2-City.mmdb" type Geolocation struct { mmdbPath string @@ -55,7 +55,7 @@ type Country struct { } func NewGeolocation(datadir string) (*Geolocation, error) { - mmdbPath := path.Join(datadir, mmdbFileName) + mmdbPath := path.Join(datadir, MMDBFileName) db, err := openDB(mmdbPath) if err != nil { @@ -167,7 +167,14 @@ func (gl *Geolocation) GetCitiesByCountry(countryISOCode string) ([]City, error) func (gl *Geolocation) Stop() error { close(gl.stopCh) if gl.db != nil { - return gl.db.Close() + if err := gl.db.Close(); err != nil { + return err + } + } + if gl.locationDB != nil { + if err := gl.locationDB.close(); err != nil { + return err + } } return nil } diff --git a/management/server/geolocation/geolocation_test.go b/management/server/geolocation/geolocation_test.go index 019baed1cbe..6fd46fcfe95 100644 --- a/management/server/geolocation/geolocation_test.go +++ b/management/server/geolocation/geolocation_test.go @@ -17,7 +17,7 @@ var mmdbPath = "../testdata/GeoLite2-City-Test.mmdb" func TestGeoLite_Lookup(t *testing.T) { tempDir := t.TempDir() - filename := path.Join(tempDir, mmdbFileName) + filename := path.Join(tempDir, MMDBFileName) err := util.CopyFileContents(mmdbPath, filename) assert.NoError(t, err) defer func() { diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 2dc92735961..9d807fdc848 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -15,7 +15,7 @@ import ( ) const ( - geoSqliteDBFile = "geonames.db" + GeoSqliteDBFile = "geonames.db" ) // SqliteStore represents a location storage backed by a Sqlite DB. @@ -27,7 +27,7 @@ type SqliteStore struct { } func NewSqliteStore(dataDir string) (*SqliteStore, error) { - file := filepath.Join(dataDir, geoSqliteDBFile) + file := filepath.Join(dataDir, GeoSqliteDBFile) db, err := connectDB(file) if err != nil { diff --git a/management/server/http/geolocation_handler_test.go b/management/server/http/geolocation_handler_test.go new file mode 100644 index 00000000000..22671100296 --- /dev/null +++ b/management/server/http/geolocation_handler_test.go @@ -0,0 +1,236 @@ +package http + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "path" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/geolocation" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/management/server/mock_server" + "github.com/netbirdio/netbird/util" +) + +func initGeolocationTestData(t *testing.T) *GeolocationsHandler { + t.Helper() + + var ( + mmdbPath = "../testdata/GeoLite2-City-Test.mmdb" + geonamesDBPath = "../testdata/geonames-test.db" + ) + + tempDir := t.TempDir() + + err := util.CopyFileContents(mmdbPath, path.Join(tempDir, geolocation.MMDBFileName)) + assert.NoError(t, err) + + err = util.CopyFileContents(geonamesDBPath, path.Join(tempDir, geolocation.GeoSqliteDBFile)) + assert.NoError(t, err) + + geo, err := geolocation.NewGeolocation(tempDir) + assert.NoError(t, err) + t.Cleanup(func() { _ = geo.Stop() }) + + return &GeolocationsHandler{ + accountManager: &mock_server.MockAccountManager{ + GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) { + user := server.NewAdminUser("test_user") + return &server.Account{ + Id: claims.AccountId, + Users: map[string]*server.User{ + "test_user": user, + }, + }, user, nil + }, + }, + geolocationManager: geo, + claimsExtractor: jwtclaims.NewClaimsExtractor( + jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { + return jwtclaims.AuthorizationClaims{ + UserId: "test_user", + Domain: "hotmail.com", + AccountId: "test_id", + } + }), + ), + } +} + +func TestGetCitiesByCountry(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedCities []api.City + requestType string + requestPath string + }{ + { + name: "Get cities with valid country iso code", + expectedStatus: http.StatusOK, + expectedBody: true, + expectedCities: []api.City{ + { + CityName: "Souni", + GeonameId: 5819, + }, + { + CityName: "Protaras", + GeonameId: 18918, + }, + }, + requestType: http.MethodGet, + requestPath: "/api/locations/countries/CY/cities", + }, + { + name: "Get cities with valid country iso code but zero cities", + expectedStatus: http.StatusOK, + expectedBody: true, + expectedCities: make([]api.City, 0), + requestType: http.MethodGet, + requestPath: "/api/locations/countries/DE/cities", + }, + { + name: "Get cities with invalid country iso code", + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + requestType: http.MethodGet, + requestPath: "/api/locations/countries/12ds/cities", + }, + } + + geolocationHandler := initGeolocationTestData(t) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/locations/countries/{country}/cities", geolocationHandler.GetCitiesByCountry).Methods("GET") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + return + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if !tc.expectedBody { + return + } + + cities := make([]api.City, 0) + if err = json.Unmarshal(content, &cities); err != nil { + t.Fatalf("unmarshal request cities response : %v", err) + return + } + assert.ElementsMatch(t, tc.expectedCities, cities) + }) + } +} + +func TestGetAllCountries(t *testing.T) { + tt := []struct { + name string + expectedStatus int + expectedBody bool + expectedCountries []api.Country + requestType string + requestPath string + }{ + { + name: "Get all countries", + expectedStatus: http.StatusOK, + expectedBody: true, + expectedCountries: []api.Country{ + { + CountryCode: "IR", + CountryName: "Iran", + }, + { + CountryCode: "CY", + CountryName: "Cyprus", + }, + { + CountryCode: "RW", + CountryName: "Rwanda", + }, + { + CountryCode: "SO", + CountryName: "Somalia", + }, + { + CountryCode: "YE", + CountryName: "Yemen", + }, + { + CountryCode: "LY", + CountryName: "Libya", + }, + { + CountryCode: "IQ", + CountryName: "Iraq", + }, + }, + requestType: http.MethodGet, + requestPath: "/api/locations/countries", + }, + } + + geolocationHandler := initGeolocationTestData(t) + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + recorder := httptest.NewRecorder() + req := httptest.NewRequest(tc.requestType, tc.requestPath, nil) + + router := mux.NewRouter() + router.HandleFunc("/api/locations/countries", geolocationHandler.GetAllCountries).Methods("GET") + router.ServeHTTP(recorder, req) + + res := recorder.Result() + defer res.Body.Close() + + content, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("I don't know what I expected; %v", err) + return + } + + if status := recorder.Code; status != tc.expectedStatus { + t.Errorf("handler returned wrong status code: got %v want %v, content: %s", + status, tc.expectedStatus, string(content)) + return + } + + if !tc.expectedBody { + return + } + + countries := make([]api.Country, 0) + if err = json.Unmarshal(content, &countries); err != nil { + t.Fatalf("unmarshal request cities response : %v", err) + return + } + assert.ElementsMatch(t, tc.expectedCountries, countries) + }) + } +} diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index 0466539fb70..a0a6d0a689f 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -42,6 +42,15 @@ func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, pos return err } + user, err := account.FindUser(userID) + if err != nil { + return err + } + + if !user.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + exists := am.savePostureChecks(account, postureChecks) if err = am.Store.SaveAccount(account); err != nil { @@ -67,6 +76,15 @@ func (am *DefaultAccountManager) DeletePostureChecks(accountID, postureChecksID, return err } + user, err := account.FindUser(userID) + if err != nil { + return err + } + + if !user.HasAdminPower() { + return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") + } + postureChecks, err := am.deletePostureChecks(account, postureChecksID) if err != nil { return err diff --git a/management/server/testdata/geonames-test.db b/management/server/testdata/geonames-test.db new file mode 100644 index 0000000000000000000000000000000000000000..3e01c096543fa3b0c9f94a8ef33b9b40824a89e9 GIT binary patch literal 16384 zcmeI3U2G#)702`a?Re8A4IzZgCf%mncDo*XY|k{i&Cb}4ojA^i?QNQcl3vFX&&2jj zckJ2VAR$%=sShB5ga8R10D(kBpMZzu2VQ`sLOh_B2c*8jxp(GzJ{$_- z4KY^M9)SXZ0)Ya70)Ya70)Ya70)Ya70)Ya70{<5Zd~UFR zbZ&96|MQ1!RB6aHS=UfgZqe&Ns+bYW8Kx{s+Zkq=zPkq1=?UEm3SRTj=ZrE;)4G#2?)dtFji!@YH-I6k4&&S-*Mi4z@F^ zs8dCX`(+nXNK#BP6k5UD=8^tH% zoDY{v7fc16pmX^9iyW#NsEKs5T`=pY(LfICW}LDXL!mk1S~AZ1SY@${{`A?etLNth z{aA5XOB}TZeHO8GlILPRYp*^nri2>miQ~bGG0K20nk9^6G~r{EawP>-&OqNby64!g zMP0VU`y?CnE79#<34x%F@j6S5=j_f+2r<9&Vqv#wAWcFY=oJN3Rl@0DYt%|{Omd_ivO8-D9J7xf24=}t+%huQ{$E>tU~bMIR(P;2Nna_@L7=C{sm zJ%0Fobb5-E9C>!k#p~4kxlrgbcAevTB63sOtW?~w2){arwdW}UT2Hi(#S;-PVl!t!pQ-;zlw@#6}{$Ir=FqKAR%c#%l7m@NfevVhBhNMP)h|L zNQ)duZk0k{C{svJg1K-gW74Wa+JBmoK+zkxN5P$_uRAVKv`1nGxptKzK_4db79*!M<(qmh>hhH&n`P>u0jT;G8AP zjfO0q)O5Im^|c!Xy#}MXrI?WvLy4Dh_yUoO$8hfR6Qx54K?$i<9rZx+OB4bF@d|-( zWFYz>xy_ua8VItP+>onha!2d+DE%=Co24E_Sgf8OCKVx_s*>J9ExpI3?L|s~d4VVu z6FA`l%2v9dR;vw}*)l6gGFK5Cz4XwsAEh)hPC=zuQn*O>@@`qLRTWt_B-22>Zsisz z2r~W>C>0kHEY3Rq#ue@q%&J+-f;)9|-03B4eT0%grmL953j)qNeo{7*m8*Id85N`; zWEgU%w|VoF1vw0h1uQZT`B^)6cFY>M1T-Dus|Qs0Fa?p>7(mHn^rF7=sGc>Arrf!p z6ZhvR4#IsA`<0CH7j@#n?ty*};#+01r~v_A%^{;EtF7Kp-Fec5oWn@Y$|(M(_Kcu{ zY#jIU^0Sl&kwH2%mLQdopC|1|cq)PEyF2)Lo)2&cR%Qr&K?z$1-!7x&b|i zo5cx9?jlPmFQK}wH+HL4)9Ar)Pgod$;KwRp;wY&APG z8(zX$dL_(pf|I4Ke3!^8=DFNzBdrW!NV2Y-({5j))^OJqY#oco@jXd_Y+3Vp8UBn{ zOW^`WV~R+txd}p;5+MCt#soI$)L<4NQb0x>ovWhZx%o@eF4IL~%JJRO&ua<6cV#0uP6ue zxEB()x}&8arR$D(0w>>YY4YO~bjym0C`^JrZ@2@{g%QtXNKWdh%db6XoEx?|hGFwF z^I?)$u7z1%z=4^$lOYGesl!y~b;E3NVzUVg1hCdyAQT9MT7eWn8j9nY0enGV2 zXn~R|>WeogTDR?T+m?8Jgc@J98pX0np+5J@ADu%1;Aq>p4?MRX=^+;eml$j$>Mt>RFnK(B+!SdrJWB@d3oA}Da?!=|>Uypxbyf8jC_Vck%jjfHo zKl-!L&y2=L-Wz#qL>YNu_}$^RhU>%3&>sLAJOTv*1p)>BO9jMZn!TZ5S%RD?@Vv8q zwz&@r#Cz%ew4yTm$k;(G)lfUI@q~BtBq%|us2W+XPy`en^8}G(or#Db#2wf;5H(nv z7IE!hC0h#EL=*tY_9_8z31^W?0I>ICHt@!UhVSow?e*m~iXw{|+?-gXy9Hu3M=WNH zirQEeJ6av#T{KT)j(Px5uDi^)0Fo%77R;sR%w1Thb*#Kk8?*jN3IIVDNMl&unL0>g zq^$jI`{MC{f7Jy5J)j9J?@WbWfX7n$1hpiS7O`|yfR>JF*l~nf36}P06?XC($ePTl z`?o(%8PJ);{iNV*1KIJgnN~ZG9yEUOZ3*221-uGo?otAHv55UoLJ{1p+AiqLEz~@P zsAzZ^lRb1{t`JO&)c;lmc>yM+oFU_$X~06>-J@%-P!NO!t_p;NwK{cy(qh_F^g4O+ z7z+o~7~~+Gkq{ETa7cIM@R+7vrlt@GE9AaWXS>{vBpL{KOOk<}q2)O3j9^#RR&w1~-%F7MAj;9W6l_-O?ySVi7TcSo<~_L}ec&0}#_p-I|P z5DTaS1*Y}vF;DGMbC8tWJsOGoMz>c|SRtyg+2xMNTRScY(Se2iK;H=Qj8l(!szA*l zCRTA^u^g^*?X?wYf}NruXLn$~wS+pfRHpr!+NKcNkO&+1Wto!4hq78XRBI(fA!~UG zv7?h0+$#v;i9NLLqPn7cnz6RkV;tk0)t$Q;R=RadxUq?i=iw*|#^qw@S{T+~?wz^q z!)+CIM-T9R^#QCC+Tv*yRbc9{_ZiBD`nnGL1$#Nlf$+9C5t8ogZ4R4bpPE2qGw>tU zVcs+FgjRvs(MR0Rx`<1}vj{n4`;UlZZ++E=+}NNTA?y z%7wW?Ffp>-;cb+J&l4rHt-(?c=38_|$`7wcdlE&_PUK@`K=@E1tkPC-5VYY8L#rvU z2P4VW)#scfQX)w*pi~mZd5_}hVj3A5QmkhmW9K$yfLBDPc!X@?d%e0Sv5T%HU}e2xm~|f}^%BP5Lty))hL6O0&*?<*@xw1Oj~{*srg;@kJK?+z z_M-6!BuBw7-J%duB`k&cNVEsy92Po|G^jyK`GXl?b%QX8W`r<=d z91aNn|A8B~zD98*8xR~hIUrNKjdR=~2sp!X+Ozi}1tcLK6MvGCZZ@<5g(4XcaQdOR T4_)9*I1BB7UT&A%<@i4Vv$T0( literal 0 HcmV?d00001 From daacd5faf753676ab678b7f4603d18b6459a0328 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 12 Feb 2024 11:46:58 +0100 Subject: [PATCH 26/35] Simplify checks definition serialisation (#1555) --- management/server/account_test.go | 3 +- .../server/http/posture_checks_handler.go | 48 +++---- .../http/posture_checks_handler_test.go | 24 ++-- management/server/policy.go | 2 +- management/server/policy_test.go | 6 +- management/server/posture/checks.go | 136 ++++++++---------- management/server/posture/checks_test.go | 30 ++-- management/server/sqlite_store.go | 1 + 8 files changed, 116 insertions(+), 134 deletions(-) diff --git a/management/server/account_test.go b/management/server/account_test.go index 2d144df72ba..6527644d520 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1562,8 +1562,7 @@ func TestAccount_Copy(t *testing.T) { DNSSettings: DNSSettings{DisabledManagementGroups: []string{}}, PostureChecks: []*posture.Checks{ { - ID: "posture Checks1", - Checks: make([]posture.Check, 0), + ID: "posture Checks1", }, }, Settings: &Settings{}, diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index bc50cfed7e1..1a9d4cda6f6 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -184,23 +184,22 @@ func (p *PostureChecksHandler) savePostureChecks( ID: postureChecksID, Name: req.Name, Description: req.Description, - Checks: make([]posture.Check, 0), } if nbVersionCheck := req.Checks.NbVersionCheck; nbVersionCheck != nil { - postureChecks.Checks = append(postureChecks.Checks, &posture.NBVersionCheck{ + postureChecks.Checks.NBVersionCheck = &posture.NBVersionCheck{ MinVersion: nbVersionCheck.MinVersion, - }) + } } if osVersionCheck := req.Checks.OsVersionCheck; osVersionCheck != nil { - postureChecks.Checks = append(postureChecks.Checks, &posture.OSVersionCheck{ + postureChecks.Checks.OSVersionCheck = &posture.OSVersionCheck{ Android: (*posture.MinVersionCheck)(osVersionCheck.Android), Darwin: (*posture.MinVersionCheck)(osVersionCheck.Darwin), Ios: (*posture.MinVersionCheck)(osVersionCheck.Ios), Linux: (*posture.MinKernelVersionCheck)(osVersionCheck.Linux), Windows: (*posture.MinKernelVersionCheck)(osVersionCheck.Windows), - }) + } } if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { @@ -208,7 +207,7 @@ func (p *PostureChecksHandler) savePostureChecks( util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) return } - postureChecks.Checks = append(postureChecks.Checks, toPostureGeoLocationCheck(geoLocationCheck)) + postureChecks.Checks.GeoLocationCheck = toPostureGeoLocationCheck(geoLocationCheck) } if err := p.accountManager.SavePostureChecks(account.Id, user.Id, &postureChecks); err != nil { @@ -271,28 +270,27 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { var checks api.Checks - for _, check := range postureChecks.Checks { - switch check.Name() { - case posture.NBVersionCheckName: - versionCheck := check.(*posture.NBVersionCheck) - checks.NbVersionCheck = &api.NBVersionCheck{ - MinVersion: versionCheck.MinVersion, - } - case posture.OSVersionCheckName: - osCheck := check.(*posture.OSVersionCheck) - checks.OsVersionCheck = &api.OSVersionCheck{ - Android: (*api.MinVersionCheck)(osCheck.Android), - Darwin: (*api.MinVersionCheck)(osCheck.Darwin), - Ios: (*api.MinVersionCheck)(osCheck.Ios), - Linux: (*api.MinKernelVersionCheck)(osCheck.Linux), - Windows: (*api.MinKernelVersionCheck)(osCheck.Windows), - } - case posture.GeoLocationCheckName: - geoLocationCheck := check.(*posture.GeoLocationCheck) - checks.GeoLocationCheck = toGeoLocationCheckResponse(geoLocationCheck) + + if postureChecks.Checks.NBVersionCheck != nil { + checks.NbVersionCheck = &api.NBVersionCheck{ + MinVersion: postureChecks.Checks.NBVersionCheck.MinVersion, + } + } + + if postureChecks.Checks.OSVersionCheck != nil { + checks.OsVersionCheck = &api.OSVersionCheck{ + Android: (*api.MinVersionCheck)(postureChecks.Checks.OSVersionCheck.Android), + Darwin: (*api.MinVersionCheck)(postureChecks.Checks.OSVersionCheck.Darwin), + Ios: (*api.MinVersionCheck)(postureChecks.Checks.OSVersionCheck.Ios), + Linux: (*api.MinKernelVersionCheck)(postureChecks.Checks.OSVersionCheck.Linux), + Windows: (*api.MinKernelVersionCheck)(postureChecks.Checks.OSVersionCheck.Windows), } } + if postureChecks.Checks.GeoLocationCheck != nil { + checks.GeoLocationCheck = toGeoLocationCheckResponse(postureChecks.Checks.GeoLocationCheck) + } + return &api.PostureCheck{ Id: postureChecks.ID, Name: postureChecks.Name, diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index 195812cc135..98ca0f99676 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -85,8 +85,8 @@ func TestGetPostureCheck(t *testing.T) { postureCheck := &posture.Checks{ ID: "postureCheck", Name: "nbVersion", - Checks: []posture.Check{ - &posture.NBVersionCheck{ + Checks: posture.ChecksDefinition{ + NBVersionCheck: &posture.NBVersionCheck{ MinVersion: "1.0.0", }, }, @@ -94,8 +94,8 @@ func TestGetPostureCheck(t *testing.T) { osPostureCheck := &posture.Checks{ ID: "osPostureCheck", Name: "osVersion", - Checks: []posture.Check{ - &posture.OSVersionCheck{ + Checks: posture.ChecksDefinition{ + OSVersionCheck: &posture.OSVersionCheck{ Linux: &posture.MinKernelVersionCheck{ MinKernelVersion: "6.0.0", }, @@ -111,8 +111,8 @@ func TestGetPostureCheck(t *testing.T) { geoPostureCheck := &posture.Checks{ ID: "geoPostureCheck", Name: "geoLocation", - Checks: []posture.Check{ - &posture.GeoLocationCheck{ + Checks: posture.ChecksDefinition{ + GeoLocationCheck: &posture.GeoLocationCheck{ Locations: []posture.Location{ { CountryCode: "DE", @@ -638,8 +638,8 @@ func TestPostureCheckUpdate(t *testing.T) { p := initPostureChecksTestData(&posture.Checks{ ID: "postureCheck", Name: "postureCheck", - Checks: []posture.Check{ - &posture.NBVersionCheck{ + Checks: posture.ChecksDefinition{ + NBVersionCheck: &posture.NBVersionCheck{ MinVersion: "1.0.0", }, }, @@ -647,8 +647,8 @@ func TestPostureCheckUpdate(t *testing.T) { &posture.Checks{ ID: "osPostureCheck", Name: "osPostureCheck", - Checks: []posture.Check{ - &posture.OSVersionCheck{ + Checks: posture.ChecksDefinition{ + OSVersionCheck: &posture.OSVersionCheck{ Linux: &posture.MinKernelVersionCheck{ MinKernelVersion: "5.0.0", }, @@ -658,8 +658,8 @@ func TestPostureCheckUpdate(t *testing.T) { &posture.Checks{ ID: "geoPostureCheck", Name: "geoLocation", - Checks: []posture.Check{ - &posture.GeoLocationCheck{ + Checks: posture.ChecksDefinition{ + GeoLocationCheck: &posture.GeoLocationCheck{ Locations: []posture.Location{ { CountryCode: "DE", diff --git a/management/server/policy.go b/management/server/policy.go index 04f6b4c76b1..291a4f1f766 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -538,7 +538,7 @@ func (a *Account) validatePostureChecksOnPeer(sourcePostureChecksID []string, pe continue } - for _, check := range postureChecks.Checks { + for _, check := range postureChecks.GetChecks() { isValid, err := check.Check(*peer) if err != nil { log.Debugf("an error occurred check %s: on peer: %s :%s", check.Name(), peer.ID, err.Error()) diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 57c33962e94..b78f52faef6 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -572,11 +572,11 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { ID: "PostureChecksDefault", Name: "Default", Description: "This is a posture checks that check if peer is running required versions", - Checks: []posture.Check{ - &posture.NBVersionCheck{ + Checks: posture.ChecksDefinition{ + NBVersionCheck: &posture.NBVersionCheck{ MinVersion: "0.25", }, - &posture.OSVersionCheck{ + OSVersionCheck: &posture.OSVersionCheck{ Linux: &posture.MinKernelVersionCheck{ MinKernelVersion: "6.6.0", }, diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 1613cf43f82..5c2356b0097 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -1,14 +1,12 @@ package posture import ( - "encoding/json" - nbpeer "github.com/netbirdio/netbird/management/server/peer" ) const ( - NBVersionCheckName = "NBVersionCheck" - OSVersionCheckName = "OSVersionCheck" + NBVersionCheckName = "NBVersionCheck" + OSVersionCheckName = "OSVersionCheck" GeoLocationCheckName = "GeoLocationCheck" ) @@ -31,8 +29,53 @@ type Checks struct { // AccountID is a reference to the Account that this object belongs AccountID string `json:"-" gorm:"index"` - // Checks is a list of objects that perform the actual checks - Checks []Check `gorm:"serializer:json"` + // Checks is a set of objects that perform the actual checks + Checks ChecksDefinition `gorm:"serializer:json"` +} + +// ChecksDefinition contains definition of actual check +type ChecksDefinition struct { + NBVersionCheck *NBVersionCheck `json:",omitempty"` + OSVersionCheck *OSVersionCheck `json:",omitempty"` + GeoLocationCheck *GeoLocationCheck `json:",omitempty"` +} + +// Copy returns a copy of a checks definition. +func (cd ChecksDefinition) Copy() ChecksDefinition { + var cdCopy ChecksDefinition + if cd.NBVersionCheck != nil { + cdCopy.NBVersionCheck = &NBVersionCheck{ + MinVersion: cd.NBVersionCheck.MinVersion, + } + } + if cd.OSVersionCheck != nil { + cdCopy.OSVersionCheck = &OSVersionCheck{} + osCheck := cdCopy.OSVersionCheck + if osCheck.Android != nil { + cdCopy.OSVersionCheck.Android = &MinVersionCheck{MinVersion: osCheck.Android.MinVersion} + } + if osCheck.Darwin != nil { + cdCopy.OSVersionCheck.Darwin = &MinVersionCheck{MinVersion: osCheck.Darwin.MinVersion} + } + if osCheck.Ios != nil { + cdCopy.OSVersionCheck.Ios = &MinVersionCheck{MinVersion: osCheck.Ios.MinVersion} + } + if osCheck.Linux != nil { + cdCopy.OSVersionCheck.Linux = &MinKernelVersionCheck{MinKernelVersion: osCheck.Linux.MinKernelVersion} + } + if osCheck.Windows != nil { + cdCopy.OSVersionCheck.Windows = &MinKernelVersionCheck{MinKernelVersion: osCheck.Windows.MinKernelVersion} + } + } + if cd.GeoLocationCheck != nil { + geoCheck := cd.GeoLocationCheck + cdCopy.GeoLocationCheck = &GeoLocationCheck{ + Action: geoCheck.Action, + Locations: make([]Location, len(geoCheck.Locations)), + } + copy(cd.GeoLocationCheck.Locations, geoCheck.Locations) + } + return cdCopy } // TableName returns the name of the table for the Checks model in the database. @@ -40,16 +83,15 @@ func (*Checks) TableName() string { return "posture_checks" } -// Copy returns a copy of a policy rule. +// Copy returns a copy of a posture checks. func (pc *Checks) Copy() *Checks { checks := &Checks{ ID: pc.ID, Name: pc.Name, Description: pc.Description, AccountID: pc.AccountID, - Checks: make([]Check, len(pc.Checks)), + Checks: pc.Checks.Copy(), } - copy(checks.Checks, pc.Checks) return checks } @@ -58,73 +100,17 @@ func (pc *Checks) EventMeta() map[string]any { return map[string]any{"name": pc.Name} } -// MarshalJSON returns the JSON encoding of the Checks object. -// The Checks object is marshaled as a map[string]json.RawMessage, -// where the key is the name of the check and the value is the JSON -// representation of the Check object. -func (pc *Checks) MarshalJSON() ([]byte, error) { - type Alias Checks - return json.Marshal(&struct { - Checks map[string]json.RawMessage - *Alias - }{ - Checks: pc.marshalChecks(), - Alias: (*Alias)(pc), - }) -} - -// UnmarshalJSON unmarshal the JSON data into the Checks object. -func (pc *Checks) UnmarshalJSON(data []byte) error { - type Alias Checks - aux := &struct { - Checks map[string]json.RawMessage - *Alias - }{ - Alias: (*Alias)(pc), +// GetChecks returns list of all initialized checks definitions +func (pc *Checks) GetChecks() []Check { + var checks []Check + if pc.Checks.NBVersionCheck != nil { + checks = append(checks, pc.Checks.NBVersionCheck) } - - if err := json.Unmarshal(data, &aux); err != nil { - return err + if pc.Checks.OSVersionCheck != nil { + checks = append(checks, pc.Checks.OSVersionCheck) } - return pc.unmarshalChecks(aux.Checks) -} - -func (pc *Checks) marshalChecks() map[string]json.RawMessage { - result := make(map[string]json.RawMessage) - for _, check := range pc.Checks { - data, err := json.Marshal(check) - if err != nil { - return result - } - result[check.Name()] = data + if pc.Checks.GeoLocationCheck != nil { + checks = append(checks, pc.Checks.GeoLocationCheck) } - return result -} - -func (pc *Checks) unmarshalChecks(rawChecks map[string]json.RawMessage) error { - pc.Checks = make([]Check, 0, len(rawChecks)) - - for name, rawCheck := range rawChecks { - switch name { - case NBVersionCheckName: - check := &NBVersionCheck{} - if err := json.Unmarshal(rawCheck, check); err != nil { - return err - } - pc.Checks = append(pc.Checks, check) - case OSVersionCheckName: - check := &OSVersionCheck{} - if err := json.Unmarshal(rawCheck, check); err != nil { - return err - } - pc.Checks = append(pc.Checks, check) - case GeoLocationCheckName: - check := &GeoLocationCheck{} - if err := json.Unmarshal(rawCheck, check); err != nil { - return err - } - pc.Checks = append(pc.Checks, check) - } - } - return nil + return checks } diff --git a/management/server/posture/checks_test.go b/management/server/posture/checks_test.go index cae82caab6a..27c3c479ca5 100644 --- a/management/server/posture/checks_test.go +++ b/management/server/posture/checks_test.go @@ -1,6 +1,7 @@ package posture import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -20,8 +21,8 @@ func TestChecks_MarshalJSON(t *testing.T) { Name: "name1", Description: "desc1", AccountID: "acc1", - Checks: []Check{ - &NBVersionCheck{ + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{ MinVersion: "1.0.0", }, }, @@ -47,8 +48,8 @@ func TestChecks_MarshalJSON(t *testing.T) { Name: "", Description: "", AccountID: "", - Checks: []Check{ - &NBVersionCheck{}, + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{}, }, }, want: []byte(` @@ -69,7 +70,7 @@ func TestChecks_MarshalJSON(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.checks.MarshalJSON() + got, err := json.Marshal(tt.checks) if (err != nil) != tt.wantErr { t.Errorf("Checks.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) return @@ -97,7 +98,6 @@ func TestChecks_UnmarshalJSON(t *testing.T) { "Description": "desc1", "Checks": { "NBVersionCheck": { - "Enabled": true, "MinVersion": "1.0.0" } } @@ -107,8 +107,8 @@ func TestChecks_UnmarshalJSON(t *testing.T) { ID: "id1", Name: "name1", Description: "desc1", - Checks: []Check{ - &NBVersionCheck{ + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{ MinVersion: "1.0.0", }, }, @@ -121,25 +121,23 @@ func TestChecks_UnmarshalJSON(t *testing.T) { expectedError: true, }, { - name: "Empty JSON Posture Check Unmarshal", - in: []byte(`{}`), - expected: &Checks{ - Checks: make([]Check, 0), - }, + name: "Empty JSON Posture Check Unmarshal", + in: []byte(`{}`), + expected: &Checks{}, expectedError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - checks := &Checks{} - err := checks.UnmarshalJSON(tc.in) + var checks Checks + err := json.Unmarshal(tc.in, &checks) if tc.expectedError { assert.Error(t, err) } else { assert.NoError(t, err) - assert.Equal(t, tc.expected, checks) + assert.Equal(t, tc.expected, &checks) } }) } diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 5788e52d45d..7a890b67466 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -375,6 +375,7 @@ func (s *SqliteStore) GetAccount(accountID string) (*Account, error) { Preload(clause.Associations). First(&account, "id = ?", accountID) if result.Error != nil { + log.Errorf("when getting account from the store: %s", result.Error) return nil, status.Errorf(status.NotFound, "account not found") } From 6cfb214ecb0334a4723d2879085f52738fee3ba5 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 12 Feb 2024 20:03:56 +0300 Subject: [PATCH 27/35] Regenerate network map on posture check update (#1563) * change network state and generate map on posture check update * Refactoring --- management/server/posture_checks.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index a0a6d0a689f..0fcabf7296a 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -52,18 +52,21 @@ func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, pos } exists := am.savePostureChecks(account, postureChecks) + action := activity.PostureCheckCreated + if exists { + action = activity.PostureCheckUpdated + account.Network.IncSerial() + } if err = am.Store.SaveAccount(account); err != nil { return err } - action := activity.PostureCheckCreated + am.StoreEvent(userID, postureChecks.ID, accountID, action, postureChecks.EventMeta()) if exists { - action = activity.PostureCheckUpdated + am.updateAccountPeers(account) } - am.StoreEvent(userID, postureChecks.ID, accountID, action, postureChecks.EventMeta()) - return nil } From 0bdf53319798c8aab92b132e81cb7d4e63e58d74 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 13 Feb 2024 12:34:25 +0100 Subject: [PATCH 28/35] Make city name optional (#1575) --- management/server/http/api/openapi.yml | 1 - management/server/http/api/types.gen.go | 2 +- management/server/http/posture_checks_handler.go | 9 +++++++-- management/server/http/posture_checks_handler_test.go | 7 +++++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 0057461049a..a60a96c69aa 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -924,7 +924,6 @@ components: example: "Berlin" required: - country_code - - city_name Country: description: Describe country geographical location information type: object diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index e75ea5aaa6f..d0c4a93ac8e 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -308,7 +308,7 @@ type GroupRequest struct { // Location Describe geographical location information type Location struct { // CityName Commonly used English name of the city - CityName string `json:"city_name"` + CityName *string `json:"city_name,omitempty"` // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country CountryCode string `json:"country_code"` diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index 1a9d4cda6f6..0e07710b3a0 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -302,8 +302,9 @@ func toPostureChecksResponse(postureChecks *posture.Checks) *api.PostureCheck { func toGeoLocationCheckResponse(geoLocationCheck *posture.GeoLocationCheck) *api.GeoLocationCheck { locations := make([]api.Location, 0, len(geoLocationCheck.Locations)) for _, loc := range geoLocationCheck.Locations { + l := loc // make G601 happy locations = append(locations, api.Location{ - CityName: loc.CityName, + CityName: &l.CityName, CountryCode: loc.CountryCode, }) } @@ -317,9 +318,13 @@ func toGeoLocationCheckResponse(geoLocationCheck *posture.GeoLocationCheck) *api func toPostureGeoLocationCheck(apiGeoLocationCheck *api.GeoLocationCheck) *posture.GeoLocationCheck { locations := make([]posture.Location, 0, len(apiGeoLocationCheck.Locations)) for _, loc := range apiGeoLocationCheck.Locations { + cityName := "" + if loc.CityName != nil { + cityName = *loc.CityName + } locations = append(locations, posture.Location{ CountryCode: loc.CountryCode, - CityName: loc.CityName, + CityName: cityName, }) } diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index 98ca0f99676..87e9ce3245a 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -21,6 +21,9 @@ import ( "github.com/netbirdio/netbird/management/server/status" ) +var berlin = "Berlin" +var losAngeles = "Los Angeles" + func initPostureChecksTestData(postureChecks ...*posture.Checks) *PostureChecksHandler { testPostureChecks := make(map[string]*posture.Checks, len(postureChecks)) for _, postureCheck := range postureChecks { @@ -342,7 +345,7 @@ func TestPostureCheckUpdate(t *testing.T) { GeoLocationCheck: &api.GeoLocationCheck{ Locations: []api.Location{ { - CityName: "Berlin", + CityName: &berlin, CountryCode: "DE", }, }, @@ -554,7 +557,7 @@ func TestPostureCheckUpdate(t *testing.T) { GeoLocationCheck: &api.GeoLocationCheck{ Locations: []api.Location{ { - CityName: "Los Angeles", + CityName: &losAngeles, CountryCode: "US", }, }, From 4982cca11debcadba95f660a72cec1a153f11d8b Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 13 Feb 2024 12:58:17 +0100 Subject: [PATCH 29/35] Do not return empty city name --- management/server/http/posture_checks_handler.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index 0e07710b3a0..ae176a13a93 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -303,8 +303,12 @@ func toGeoLocationCheckResponse(geoLocationCheck *posture.GeoLocationCheck) *api locations := make([]api.Location, 0, len(geoLocationCheck.Locations)) for _, loc := range geoLocationCheck.Locations { l := loc // make G601 happy + var cityName *string + if loc.CityName != "" { + cityName = &l.CityName + } locations = append(locations, api.Location{ - CityName: &l.CityName, + CityName: cityName, CountryCode: loc.CountryCode, }) } From 5d4039658ca91bc9fe537ee4907e05f3e09329b2 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 13 Feb 2024 15:05:26 +0100 Subject: [PATCH 30/35] Validate action param of geo location checks (#1577) We only support allow and deny --- .../server/http/posture_checks_handler.go | 6 ++++- .../http/posture_checks_handler_test.go | 25 +++++++++++++++++++ management/server/posture/geo_location.go | 2 ++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index ae176a13a93..a33ef4b3631 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" "regexp" + "slices" "github.com/gorilla/mux" "github.com/rs/xid" @@ -250,10 +251,13 @@ func validatePostureChecksUpdate(req api.PostureCheckUpdate) error { if geoLocationCheck.Action == "" { return status.Errorf(status.InvalidArgument, "action for geolocation check shouldn't be empty") } + allowedActions := []api.GeoLocationCheckAction{api.GeoLocationCheckActionAllow, api.GeoLocationCheckActionDeny} + if !slices.Contains(allowedActions, geoLocationCheck.Action) { + return status.Errorf(status.InvalidArgument, "action for geolocation check is not valid value") + } if len(geoLocationCheck.Locations) == 0 { return status.Errorf(status.InvalidArgument, "locations for geolocation check shouldn't be empty") } - for _, loc := range geoLocationCheck.Locations { if loc.CountryCode == "" { return status.Errorf(status.InvalidArgument, "country code for geolocation check shouldn't be empty") diff --git a/management/server/http/posture_checks_handler_test.go b/management/server/http/posture_checks_handler_test.go index 87e9ce3245a..ac757252b6b 100644 --- a/management/server/http/posture_checks_handler_test.go +++ b/management/server/http/posture_checks_handler_test.go @@ -591,6 +591,31 @@ func TestPostureCheckUpdate(t *testing.T) { handler.geolocationManager = nil }, }, + { + name: "Update Posture Checks Geo Location with not valid action", + requestType: http.MethodPut, + requestPath: "/api/posture-checks/geoPostureCheck", + requestBody: bytes.NewBuffer( + []byte(`{ + "name": "default", + "checks": { + "geo_location_check": { + "locations": [ + { + "city_name": "Los Angeles", + "country_code": "US" + } + ], + "action": "not-valid" + } + } + }`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + setupHandlerFunc: func(handler *PostureChecksHandler) { + handler.geolocationManager = nil + }, + }, { name: "Update Posture Checks Invalid Check", requestType: http.MethodPut, diff --git a/management/server/posture/geo_location.go b/management/server/posture/geo_location.go index ceade0d2ae5..d23c643bc3b 100644 --- a/management/server/posture/geo_location.go +++ b/management/server/posture/geo_location.go @@ -43,6 +43,8 @@ func (g *GeoLocationCheck) Check(peer nbpeer.Peer) (bool, error) { return false, nil case GeoLocationActionAllow: return true, nil + default: + return false, fmt.Errorf("invalid geo location action: %s", g.Action) } } } From d3904c7d5f60fe084ff5b484a9c010f59e0c7d8f Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 13 Feb 2024 15:56:43 +0100 Subject: [PATCH 31/35] Switch realip middleware to upstream (#1578) --- go.mod | 4 +--- go.sum | 4 ++-- management/cmd/management.go | 14 +++++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 2bbedd974a7..64444616159 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/google/go-cmp v0.5.9 github.com/google/gopacket v1.1.19 github.com/google/nftables v0.0.0-20220808154552-2eca00135732 - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240202184442-37827591b26c + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 github.com/hashicorp/go-version v1.6.0 github.com/libp2p/go-netroute v0.2.0 @@ -172,5 +172,3 @@ replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-202 replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20240105182236-6c340dd55aed replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 - -replace github.com/grpc-ecosystem/go-grpc-middleware/v2 => github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f diff --git a/go.sum b/go.sum index 0a16eefe6c7..664e8a6f226 100644 --- a/go.sum +++ b/go.sum @@ -286,6 +286,8 @@ github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0 h1:fWY+zXdWhvWnd github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357 h1:Fkzd8ktnpOR9h47SXHe2AYPwelXLH2GjGsjlAloiWfo= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= @@ -519,8 +521,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f h1:J+egXEDkpg/vOYYzPO5IwF8OufGb7g+KcwEF1AWIzhQ= -github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4= github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0= github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= diff --git a/management/cmd/management.go b/management/cmd/management.go index bf0865f731c..fc7417f77f6 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -191,17 +191,17 @@ var ( log.Warn("TrustedHTTPProxies and TrustedHTTPProxiesCount both are configured. " + "This is not recommended way to extract X-Forwarded-For. Consider using one of these options.") } - realipOpts := realip.Opts{ - TrustedPeers: trustedPeers, - TrustedProxies: trustedHTTPProxies, - TrustedProxiesCount: trustedProxiesCount, - Headers: []string{realip.XForwardedFor, realip.XRealIp}, + realipOpts := []realip.Option{ + realip.WithTrustedPeers(trustedPeers), + realip.WithTrustedProxies(trustedHTTPProxies), + realip.WithTrustedProxiesCount(trustedProxiesCount), + realip.WithHeaders([]string{realip.XForwardedFor, realip.XRealIp}), } gRPCOpts := []grpc.ServerOption{ grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp), - grpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(realipOpts)), - grpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(realipOpts)), + grpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(realipOpts...)), + grpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(realipOpts...)), } var certManager *autocert.Manager From 91ebbc0291e888d5631550230803488fbfc7e807 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 13 Feb 2024 16:39:38 +0100 Subject: [PATCH 32/35] Be more silent in download-geolite2.sh script --- infrastructure_files/download-geolite2.sh | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/infrastructure_files/download-geolite2.sh b/infrastructure_files/download-geolite2.sh index 7cc5528ae29..22ccb6ecb6c 100755 --- a/infrastructure_files/download-geolite2.sh +++ b/infrastructure_files/download-geolite2.sh @@ -33,9 +33,9 @@ download_geolite_mmdb() { SIGNATURE_URL="https://download.maxmind.com/geoip/databases/GeoLite2-City/download?suffix=tar.gz.sha256" # Download the database and signature files - echo "Downloading database file..." + echo "Downloading mmdb database file..." DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") - echo "Downloading signature file..." + echo "Downloading mmdb signature file..." SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") # Verify the signature @@ -51,14 +51,14 @@ download_geolite_mmdb() { EXTRACTION_DIR=$(basename "$DATABASE_FILE" .tar.gz) echo "Unpacking $DATABASE_FILE..." mkdir -p "$EXTRACTION_DIR" - tar -xzvf "$DATABASE_FILE" + tar -xzvf "$DATABASE_FILE" > /dev/null 2>&1 # Create a SHA256 signature file MMDB_FILE="GeoLite2-City.mmdb" cd "$EXTRACTION_DIR" sha256sum "$MMDB_FILE" > "$MMDB_FILE.sha256" echo "SHA256 signature created for $MMDB_FILE." - cd - + cd - > /dev/null 2>&1 # Remove downloaded files rm "$DATABASE_FILE" "$SIGNATURE_FILE" @@ -76,9 +76,9 @@ download_geolite_csv_and_create_sqlite_db() { # Download the database file - echo "Downloading database file..." + echo "Downloading csv database file..." DATABASE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$DATABASE_URL" -w "%{filename_effective}") - echo "Downloading signature file..." + echo "Downloading csv signature file..." SIGNATURE_FILE=$(curl -s -u "$MM_ACCOUNT_ID":"$MM_LICENSE_KEY" -L -O -J "$SIGNATURE_URL" -w "%{filename_effective}") # Verify the signature @@ -94,7 +94,8 @@ download_geolite_csv_and_create_sqlite_db() { EXTRACTION_DIR=$(basename "$DATABASE_FILE" .zip) DB_NAME="geonames.db" - unzip "$DATABASE_FILE" + echo "Unpacking $DATABASE_FILE..." + unzip "$DATABASE_FILE" > /dev/null 2>&1 # Create SQLite database and import data from CSV sqlite3 "$DB_NAME" < Date: Tue, 13 Feb 2024 19:28:24 +0300 Subject: [PATCH 33/35] Fix geonames db reload (#1580) --- management/server/geolocation/store.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/management/server/geolocation/store.go b/management/server/geolocation/store.go index 9d807fdc848..9f3638a7c5d 100644 --- a/management/server/geolocation/store.go +++ b/management/server/geolocation/store.go @@ -12,6 +12,8 @@ import ( "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" + + "github.com/netbirdio/netbird/management/server/status" ) const ( @@ -23,6 +25,7 @@ type SqliteStore struct { db *gorm.DB filePath string mux sync.RWMutex + closed bool sha256sum []byte } @@ -52,6 +55,10 @@ func (s *SqliteStore) GetAllCountries() ([]Country, error) { s.mux.RLock() defer s.mux.RUnlock() + if s.closed { + return nil, status.Errorf(status.PreconditionFailed, "geo location database is not initialized") + } + var countries []Country result := s.db.Table("geonames"). Select("country_iso_code", "country_name"). @@ -60,6 +67,7 @@ func (s *SqliteStore) GetAllCountries() ([]Country, error) { if result.Error != nil { return nil, result.Error } + return countries, nil } @@ -68,6 +76,10 @@ func (s *SqliteStore) GetCitiesByCountry(countryISOCode string) ([]City, error) s.mux.RLock() defer s.mux.RUnlock() + if s.closed { + return nil, status.Errorf(status.PreconditionFailed, "geo location database is not initialized") + } + var cities []City result := s.db.Table("geonames"). Select("geoname_id", "city_name"). @@ -104,13 +116,15 @@ func (s *SqliteStore) reload() error { } log.Infof("Reloading '%s'", s.filePath) + _ = s.close() + s.closed = true newDb, err := connectDB(s.filePath) if err != nil { return err } - _ = s.close() + s.closed = false s.db = newDb log.Infof("Successfully reloaded '%s'", s.filePath) From 08e590792a71a25ec8d9700a1c2bca1cf5a619ab Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 19 Feb 2024 17:06:21 +0100 Subject: [PATCH 34/35] Ensure posture check name uniqueness when create (#1594) --- management/server/http/api/openapi.yml | 2 +- management/server/posture_checks.go | 17 +++- management/server/posture_checks_test.go | 118 +++++++++++++++++++++++ 3 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 management/server/posture_checks_test.go diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index a60a96c69aa..a1030e54e45 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -828,7 +828,7 @@ components: type: string example: ch8i4ug6lnn4g9hqv7mg name: - description: Posture check name identifier + description: Posture check unique name identifier type: string example: Default description: diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index 0fcabf7296a..0bb6644ed1d 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -51,7 +51,13 @@ func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, pos return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") } - exists := am.savePostureChecks(account, postureChecks) + exists, uniqName := am.savePostureChecks(account, postureChecks) + + // we do not allow create new posture checks with non uniq name + if !exists && !uniqName { + return status.Errorf(status.PreconditionFailed, "Posture check name should be unique") + } + action := activity.PostureCheckCreated if exists { action = activity.PostureCheckUpdated @@ -123,12 +129,15 @@ func (am *DefaultAccountManager) ListPostureChecks(accountID, userID string) ([] return account.PostureChecks, nil } -func (am *DefaultAccountManager) savePostureChecks(account *Account, postureChecks *posture.Checks) (exists bool) { +func (am *DefaultAccountManager) savePostureChecks(account *Account, postureChecks *posture.Checks) (exists, uniqName bool) { + uniqName = true for i, p := range account.PostureChecks { - if p.ID == postureChecks.ID { + if !exists && p.ID == postureChecks.ID { account.PostureChecks[i] = postureChecks exists = true - break + } + if p.Name == postureChecks.Name { + uniqName = false } } if !exists { diff --git a/management/server/posture_checks_test.go b/management/server/posture_checks_test.go new file mode 100644 index 00000000000..a65cb8c53e6 --- /dev/null +++ b/management/server/posture_checks_test.go @@ -0,0 +1,118 @@ +package server + +import ( + "testing" + + "github.com/netbirdio/netbird/management/server/posture" + "github.com/stretchr/testify/assert" +) + +const ( + adminUserID = "adminUserID" + regularUserID = "regularUserID" + postureCheckID = "existing-id" + postureCheckName = "Existing check" +) + +func TestDefaultAccountManager_PostureCheck(t *testing.T) { + am, err := createManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestPostureChecksAccount(am) + if err != nil { + t.Error("failed to init testing account") + } + + t.Run("Generic posture check flow", func(t *testing.T) { + // regular users can not create checks + err := am.SavePostureChecks(account.Id, regularUserID, &posture.Checks{}) + assert.Error(t, err) + + // regular users cannot list check + _, err = am.ListPostureChecks(account.Id, regularUserID) + assert.Error(t, err) + + // should be possible to create posture check with uniq name + err = am.SavePostureChecks(account.Id, adminUserID, &posture.Checks{ + ID: postureCheckID, + Name: postureCheckName, + Checks: posture.ChecksDefinition{ + NBVersionCheck: &posture.NBVersionCheck{ + MinVersion: "0.26.0", + }, + }, + }) + assert.NoError(t, err) + + // admin users can list check + checks, err := am.ListPostureChecks(account.Id, adminUserID) + assert.NoError(t, err) + assert.Len(t, checks, 1) + + // should not be possible to create posture check with non uniq name + err = am.SavePostureChecks(account.Id, adminUserID, &posture.Checks{ + ID: "new-id", + Name: postureCheckName, + Checks: posture.ChecksDefinition{ + GeoLocationCheck: &posture.GeoLocationCheck{ + Locations: []posture.Location{ + { + CountryCode: "DE", + }, + }, + }, + }, + }) + assert.Error(t, err) + + // admins can update posture checks + err = am.SavePostureChecks(account.Id, adminUserID, &posture.Checks{ + ID: postureCheckID, + Name: postureCheckName, + Checks: posture.ChecksDefinition{ + NBVersionCheck: &posture.NBVersionCheck{ + MinVersion: "0.27.0", + }, + }, + }) + assert.NoError(t, err) + + // users should not be able to delete posture checks + err = am.DeletePostureChecks(account.Id, postureCheckID, regularUserID) + assert.Error(t, err) + + // admin should be able to delete posture checks + err = am.DeletePostureChecks(account.Id, postureCheckID, adminUserID) + assert.NoError(t, err) + checks, err = am.ListPostureChecks(account.Id, adminUserID) + assert.NoError(t, err) + assert.Len(t, checks, 0) + }) +} + +func initTestPostureChecksAccount(am *DefaultAccountManager) (*Account, error) { + accountID := "testingAccount" + domain := "example.com" + + admin := &User{ + Id: adminUserID, + Role: UserRoleAdmin, + } + user := &User{ + Id: regularUserID, + Role: UserRoleUser, + } + + account := newAccountWithId(accountID, groupAdminUserID, domain) + account.Users[admin.Id] = admin + account.Users[user.Id] = user + + err := am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + return am.Store.GetAccount(account.Id) +} From 6f2841800f862af6a1672028134ac3194285c032 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Mon, 19 Feb 2024 20:57:36 +0300 Subject: [PATCH 35/35] Enhance the management of posture checks (#1595) * add a correct min version and kernel for os posture check example * handle error when geo or location db is nil * expose all peer location details in api response * Check for nil geolocation manager only * Validate posture check before save * bump open api version * add peer location fields to toPeerListItemResponse --- management/server/http/api/openapi.yml | 43 +++++++---- management/server/http/api/types.gen.go | 55 +++++++++----- .../server/http/geolocations_handler.go | 12 +++ management/server/http/handler.go | 9 +-- management/server/http/peers_handler.go | 4 + .../server/http/posture_checks_handler.go | 1 + management/server/posture/checks.go | 61 +++++++++++++++ management/server/posture/checks_test.go | 74 +++++++++++++++++++ management/server/posture_checks.go | 4 + 9 files changed, 225 insertions(+), 38 deletions(-) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index a1030e54e45..0663d964a2e 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -1,4 +1,4 @@ -openapi: 3.0.1 +openapi: 3.1.0 servers: - url: https://api.netbird.io description: Default server @@ -318,6 +318,10 @@ components: description: (Cloud only) Indicates whether peer needs approval type: boolean example: true + country_code: + $ref: '#/components/schemas/CountryCode' + city_name: + $ref: '#/components/schemas/CityName' required: - ip - connected @@ -868,11 +872,22 @@ components: description: Minimum version of iOS $ref: '#/components/schemas/MinVersionCheck' linux: - description: Minimum version of Linux + description: Minimum Linux kernel version $ref: '#/components/schemas/MinKernelVersionCheck' windows: - description: Minimum version of Windows + description: Minimum Windows kernel build version $ref: '#/components/schemas/MinKernelVersionCheck' + example: + android: + min_version: "13" + ios: + min_version: "17.3.1" + darwin: + min_version: "14.2.1" + linux: + min_kernel_version: "5.3.3" + windows: + min_kernel_version: "10.0.1234" MinVersionCheck: description: Posture check for the version of operating system type: object @@ -884,7 +899,7 @@ components: required: - min_version MinKernelVersionCheck: - description: Posture check for the version of kernel + description: Posture check with the kernel version type: object properties: min_kernel_version: @@ -915,15 +930,19 @@ components: type: object properties: country_code: - description: 2-letter ISO 3166-1 alpha-2 code that represents the country - type: string - example: "DE" + $ref: '#/components/schemas/CountryCode' city_name: - description: Commonly used English name of the city - type: string - example: "Berlin" + $ref: '#/components/schemas/CityName' required: - country_code + CountryCode: + description: 2-letter ISO 3166-1 alpha-2 code that represents the country + type: string + example: "DE" + CityName: + description: Commonly used English name of the city + type: string + example: "Berlin" Country: description: Describe country geographical location information type: object @@ -933,9 +952,7 @@ components: type: string example: "Germany" country_code: - description: 2-letter ISO 3166-1 alpha-2 code that represents the country - type: string - example: "DE" + $ref: '#/components/schemas/CountryCode' required: - country_name - country_code diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index d0c4a93ac8e..87e6b9be325 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -186,9 +186,7 @@ type AccountSettings struct { type Checks struct { // GeoLocationCheck Posture check for geo location GeoLocationCheck *GeoLocationCheck `json:"geo_location_check,omitempty"` - - // NbVersionCheck Posture check for the version of operating system - NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` + NbVersionCheck *NBVersionCheck `json:"nb_version_check,omitempty"` // OsVersionCheck Posture check for the version of operating system OsVersionCheck *OSVersionCheck `json:"os_version_check,omitempty"` @@ -203,15 +201,21 @@ type City struct { GeonameId int `json:"geoname_id"` } +// CityName Commonly used English name of the city +type CityName = string + // Country Describe country geographical location information type Country struct { // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country - CountryCode string `json:"country_code"` + CountryCode CountryCode `json:"country_code"` // CountryName Commonly used English name of the country CountryName string `json:"country_name"` } +// CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country +type CountryCode = string + // DNSSettings defines model for DNSSettings. type DNSSettings struct { // DisabledManagementGroups Groups whose DNS management is disabled @@ -308,25 +312,25 @@ type GroupRequest struct { // Location Describe geographical location information type Location struct { // CityName Commonly used English name of the city - CityName *string `json:"city_name,omitempty"` + CityName *CityName `json:"city_name,omitempty"` // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country - CountryCode string `json:"country_code"` + CountryCode CountryCode `json:"country_code"` } -// MinKernelVersionCheck Posture check for the version of kernel +// MinKernelVersionCheck Posture check with the kernel version type MinKernelVersionCheck struct { // MinKernelVersion Minimum acceptable version MinKernelVersion string `json:"min_kernel_version"` } -// MinVersionCheck Posture check for the version of operating system +// MinVersionCheck defines model for MinVersionCheck. type MinVersionCheck struct { // MinVersion Minimum acceptable version MinVersion string `json:"min_version"` } -// NBVersionCheck Posture check for the version of operating system +// NBVersionCheck defines model for NBVersionCheck. type NBVersionCheck = MinVersionCheck // Nameserver defines model for Nameserver. @@ -403,19 +407,14 @@ type NameserverGroupRequest struct { // OSVersionCheck Posture check for the version of operating system type OSVersionCheck struct { - // Android Posture check for the version of operating system Android *MinVersionCheck `json:"android,omitempty"` + Darwin *MinVersionCheck `json:"darwin,omitempty"` + Ios *MinVersionCheck `json:"ios,omitempty"` - // Darwin Posture check for the version of operating system - Darwin *MinVersionCheck `json:"darwin,omitempty"` - - // Ios Posture check for the version of operating system - Ios *MinVersionCheck `json:"ios,omitempty"` - - // Linux Posture check for the version of kernel + // Linux Posture check with the kernel version Linux *MinKernelVersionCheck `json:"linux,omitempty"` - // Windows Posture check for the version of kernel + // Windows Posture check with the kernel version Windows *MinKernelVersionCheck `json:"windows,omitempty"` } @@ -427,12 +426,18 @@ type Peer struct { // ApprovalRequired (Cloud only) Indicates whether peer needs approval ApprovalRequired *bool `json:"approval_required,omitempty"` + // CityName Commonly used English name of the city + CityName *CityName `json:"city_name,omitempty"` + // Connected Peer to Management connection status Connected bool `json:"connected"` // ConnectionIp Peer's public connection IP address ConnectionIp *string `json:"connection_ip,omitempty"` + // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country + CountryCode *CountryCode `json:"country_code,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` @@ -490,12 +495,18 @@ type PeerBase struct { // ApprovalRequired (Cloud only) Indicates whether peer needs approval ApprovalRequired *bool `json:"approval_required,omitempty"` + // CityName Commonly used English name of the city + CityName *CityName `json:"city_name,omitempty"` + // Connected Peer to Management connection status Connected bool `json:"connected"` // ConnectionIp Peer's public connection IP address ConnectionIp *string `json:"connection_ip,omitempty"` + // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country + CountryCode *CountryCode `json:"country_code,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` @@ -556,12 +567,18 @@ type PeerBatch struct { // ApprovalRequired (Cloud only) Indicates whether peer needs approval ApprovalRequired *bool `json:"approval_required,omitempty"` + // CityName Commonly used English name of the city + CityName *CityName `json:"city_name,omitempty"` + // Connected Peer to Management connection status Connected bool `json:"connected"` // ConnectionIp Peer's public connection IP address ConnectionIp *string `json:"connection_ip,omitempty"` + // CountryCode 2-letter ISO 3166-1 alpha-2 code that represents the country + CountryCode *CountryCode `json:"country_code,omitempty"` + // DnsLabel Peer's DNS label is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's domain to the peer label. e.g. peer-dns-label.netbird.cloud DnsLabel string `json:"dns_label"` @@ -849,7 +866,7 @@ type PostureCheck struct { // Id Posture check ID Id string `json:"id"` - // Name Posture check name identifier + // Name Posture check unique name identifier Name string `json:"name"` } diff --git a/management/server/http/geolocations_handler.go b/management/server/http/geolocations_handler.go index 30958968774..070aa6350a5 100644 --- a/management/server/http/geolocations_handler.go +++ b/management/server/http/geolocations_handler.go @@ -39,6 +39,12 @@ func (l *GeolocationsHandler) GetAllCountries(w http.ResponseWriter, r *http.Req return } + if l.geolocationManager == nil { + // TODO: update error message to include geo db self hosted doc link when ready + util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) + return + } + allCountries, err := l.geolocationManager.GetAllCountries() if err != nil { util.WriteError(err, w) @@ -66,6 +72,12 @@ func (l *GeolocationsHandler) GetCitiesByCountry(w http.ResponseWriter, r *http. return } + if l.geolocationManager == nil { + // TODO: update error message to include geo db self hosted doc link when ready + util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) + return + } + allCities, err := l.geolocationManager.GetCitiesByCountry(countryCode) if err != nil { util.WriteError(err, w) diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 168b05571f0..75c4b277d74 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -216,10 +216,7 @@ func (apiHandler *apiHandler) addPostureCheckEndpoint() { } func (apiHandler *apiHandler) addLocationsEndpoint() { - // enable location endpoints if location manager is enabled - if apiHandler.geolocationManager != nil { - locationHandler := NewGeolocationsHandlerHandler(apiHandler.AccountManager, apiHandler.geolocationManager, apiHandler.AuthCfg) - apiHandler.Router.HandleFunc("/locations/countries", locationHandler.GetAllCountries).Methods("GET", "OPTIONS") - apiHandler.Router.HandleFunc("/locations/countries/{country}/cities", locationHandler.GetCitiesByCountry).Methods("GET", "OPTIONS") - } + locationHandler := NewGeolocationsHandlerHandler(apiHandler.AccountManager, apiHandler.geolocationManager, apiHandler.AuthCfg) + apiHandler.Router.HandleFunc("/locations/countries", locationHandler.GetAllCountries).Methods("GET", "OPTIONS") + apiHandler.Router.HandleFunc("/locations/countries/{country}/cities", locationHandler.GetCitiesByCountry).Methods("GET", "OPTIONS") } diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index 66ecc4bf7d5..e44b164b777 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -267,6 +267,8 @@ func toSinglePeerResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dnsD LoginExpired: peer.Status.LoginExpired, AccessiblePeers: accessiblePeer, ApprovalRequired: &peer.Status.RequiresApproval, + CountryCode: &peer.Location.CountryCode, + CityName: &peer.Location.CityName, } } @@ -298,6 +300,8 @@ func toPeerListItemResponse(peer *nbpeer.Peer, groupsInfo []api.GroupMinimum, dn LoginExpired: peer.Status.LoginExpired, AccessiblePeersCount: accessiblePeersCount, ApprovalRequired: &peer.Status.RequiresApproval, + CountryCode: &peer.Location.CountryCode, + CityName: &peer.Location.CityName, } } diff --git a/management/server/http/posture_checks_handler.go b/management/server/http/posture_checks_handler.go index a33ef4b3631..2f27e2579c0 100644 --- a/management/server/http/posture_checks_handler.go +++ b/management/server/http/posture_checks_handler.go @@ -205,6 +205,7 @@ func (p *PostureChecksHandler) savePostureChecks( if geoLocationCheck := req.Checks.GeoLocationCheck; geoLocationCheck != nil { if p.geolocationManager == nil { + // TODO: update error message to include geo db self hosted doc link when ready util.WriteError(status.Errorf(status.PreconditionFailed, "Geo location database is not initialized"), w) return } diff --git a/management/server/posture/checks.go b/management/server/posture/checks.go index 5c2356b0097..c5e66bceac5 100644 --- a/management/server/posture/checks.go +++ b/management/server/posture/checks.go @@ -1,6 +1,10 @@ package posture import ( + "fmt" + + "github.com/hashicorp/go-version" + nbpeer "github.com/netbirdio/netbird/management/server/peer" ) @@ -114,3 +118,60 @@ func (pc *Checks) GetChecks() []Check { } return checks } + +func (pc *Checks) Validate() error { + if check := pc.Checks.NBVersionCheck; check != nil { + if !isVersionValid(check.MinVersion) { + return fmt.Errorf("%s version: %s is not valid", check.Name(), check.MinVersion) + } + } + + if osCheck := pc.Checks.OSVersionCheck; osCheck != nil { + if osCheck.Android != nil { + if !isVersionValid(osCheck.Android.MinVersion) { + return fmt.Errorf("%s android version: %s is not valid", osCheck.Name(), osCheck.Android.MinVersion) + } + } + + if osCheck.Ios != nil { + if !isVersionValid(osCheck.Ios.MinVersion) { + return fmt.Errorf("%s ios version: %s is not valid", osCheck.Name(), osCheck.Ios.MinVersion) + } + } + + if osCheck.Darwin != nil { + if !isVersionValid(osCheck.Darwin.MinVersion) { + return fmt.Errorf("%s darwin version: %s is not valid", osCheck.Name(), osCheck.Darwin.MinVersion) + } + } + + if osCheck.Linux != nil { + if !isVersionValid(osCheck.Linux.MinKernelVersion) { + return fmt.Errorf("%s linux kernel version: %s is not valid", osCheck.Name(), + osCheck.Linux.MinKernelVersion) + } + } + + if osCheck.Windows != nil { + if !isVersionValid(osCheck.Windows.MinKernelVersion) { + return fmt.Errorf("%s windows kernel version: %s is not valid", osCheck.Name(), + osCheck.Windows.MinKernelVersion) + } + } + } + + return nil +} + +func isVersionValid(ver string) bool { + newVersion, err := version.NewVersion(ver) + if err != nil { + return false + } + + if newVersion != nil { + return true + } + + return false +} diff --git a/management/server/posture/checks_test.go b/management/server/posture/checks_test.go index 27c3c479ca5..ef6eefeecfd 100644 --- a/management/server/posture/checks_test.go +++ b/management/server/posture/checks_test.go @@ -142,3 +142,77 @@ func TestChecks_UnmarshalJSON(t *testing.T) { }) } } + +func TestChecks_Validate(t *testing.T) { + testCases := []struct { + name string + checks Checks + expectedError bool + }{ + { + name: "Valid checks version", + checks: Checks{ + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{ + MinVersion: "0.25.0", + }, + OSVersionCheck: &OSVersionCheck{ + Ios: &MinVersionCheck{ + MinVersion: "13.0.1", + }, + Linux: &MinKernelVersionCheck{ + MinKernelVersion: "5.3.3-dev", + }, + }, + }, + }, + expectedError: false, + }, + { + name: "Invalid checks version", + checks: Checks{ + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{ + MinVersion: "abc", + }, + OSVersionCheck: &OSVersionCheck{ + Android: &MinVersionCheck{ + MinVersion: "dev", + }, + }, + }, + }, + expectedError: true, + }, + { + name: "Combined valid and invalid checks version", + checks: Checks{ + Checks: ChecksDefinition{ + NBVersionCheck: &NBVersionCheck{ + MinVersion: "abc", + }, + OSVersionCheck: &OSVersionCheck{ + Windows: &MinKernelVersionCheck{ + MinKernelVersion: "10.0.1234", + }, + Darwin: &MinVersionCheck{ + MinVersion: "13.0.1", + }, + }, + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.checks.Validate() + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/management/server/posture_checks.go b/management/server/posture_checks.go index 0bb6644ed1d..7e654b5fb7c 100644 --- a/management/server/posture_checks.go +++ b/management/server/posture_checks.go @@ -51,6 +51,10 @@ func (am *DefaultAccountManager) SavePostureChecks(accountID, userID string, pos return status.Errorf(status.PermissionDenied, "only users with admin power are allowed to view posture checks") } + if err := postureChecks.Validate(); err != nil { + return status.Errorf(status.BadRequest, err.Error()) + } + exists, uniqName := am.savePostureChecks(account, postureChecks) // we do not allow create new posture checks with non uniq name