mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 08:36:37 +00:00
Compare commits
46 Commits
handle-use
...
v0.23.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b90ff8c24 | ||
|
|
b8599f634c | ||
|
|
659110f0d5 | ||
|
|
4ad14cb46b | ||
|
|
3c485dc7a1 | ||
|
|
f7e6cdcbf0 | ||
|
|
af6fdd3af2 | ||
|
|
5781ec7a8e | ||
|
|
1219006a6e | ||
|
|
4791e41004 | ||
|
|
9131069d12 | ||
|
|
26bbc33e7a | ||
|
|
35bc493cc3 | ||
|
|
e26ec0b937 | ||
|
|
a952e7c72f | ||
|
|
22f69d7852 | ||
|
|
b23011fbe8 | ||
|
|
6ad3894a51 | ||
|
|
c81b83b346 | ||
|
|
8c5c6815e0 | ||
|
|
0c470e7838 | ||
|
|
8118d60ffb | ||
|
|
1956ca169e | ||
|
|
830dee1771 | ||
|
|
c08a96770e | ||
|
|
c6bf1c7f26 | ||
|
|
5f499d66b2 | ||
|
|
7c065bd9fc | ||
|
|
ab849f0942 | ||
|
|
aa1d31bde6 | ||
|
|
5b4dc4dd47 | ||
|
|
1324169ebb | ||
|
|
732afd8393 | ||
|
|
da7b6b11ad | ||
|
|
e260270825 | ||
|
|
d4b6d7646c | ||
|
|
8febab4076 | ||
|
|
34e2c6b943 | ||
|
|
0be8c72601 | ||
|
|
c34e53477f | ||
|
|
8d18190c94 | ||
|
|
06bec61be9 | ||
|
|
2135533f1d | ||
|
|
bb791d59f3 | ||
|
|
30f1c54ed1 | ||
|
|
5c8541ef42 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.go text eol=lf
|
||||||
41
.github/workflows/android-build-validation.yml
vendored
Normal file
41
.github/workflows/android-build-validation.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Android build validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.20.x"
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v2
|
||||||
|
- name: NDK Cache
|
||||||
|
id: ndk-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /usr/local/lib/android/sdk/ndk
|
||||||
|
key: ndk-cache-23.1.7779620
|
||||||
|
- name: Setup NDK
|
||||||
|
run: /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;23.1.7779620"
|
||||||
|
- name: install gomobile
|
||||||
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20230531173138-3c911d8e3eda
|
||||||
|
- name: gomobile init
|
||||||
|
run: gomobile init
|
||||||
|
- name: build android nebtird lib
|
||||||
|
run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620
|
||||||
20
.github/workflows/golangci-lint.yml
vendored
20
.github/workflows/golangci-lint.yml
vendored
@@ -1,12 +1,23 @@
|
|||||||
name: golangci-lint
|
name: golangci-lint
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci:
|
golangci:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
name: lint
|
name: lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -14,7 +25,12 @@ jobs:
|
|||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.20.x"
|
||||||
|
cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
args: --timeout=12m
|
||||||
25
.github/workflows/test-infrastructure-files.yml
vendored
25
.github/workflows/test-infrastructure-files.yml
vendored
@@ -80,6 +80,7 @@ jobs:
|
|||||||
CI_NETBIRD_MGMT_IDP: "none"
|
CI_NETBIRD_MGMT_IDP: "none"
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
|
CI_NETBIRD_SIGNAL_PORT: 12345
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
@@ -91,6 +92,7 @@ jobs:
|
|||||||
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "$CI_NETBIRD_DOMAIN:33073"
|
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "$CI_NETBIRD_DOMAIN:33073"
|
||||||
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
|
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
|
||||||
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
|
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
|
||||||
|
grep $CI_NETBIRD_SIGNAL_PORT docker-compose.yml | grep ':80'
|
||||||
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
|
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
|
||||||
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
|
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
|
||||||
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
|
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
|
||||||
@@ -110,6 +112,27 @@ jobs:
|
|||||||
grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
||||||
grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Build management binary
|
||||||
|
working-directory: management
|
||||||
|
run: CGO_ENABLED=1 go build -o netbird-mgmt main.go
|
||||||
|
|
||||||
|
- name: Build management docker image
|
||||||
|
working-directory: management
|
||||||
|
run: |
|
||||||
|
docker build -t netbirdio/management:latest .
|
||||||
|
|
||||||
|
- name: Build signal binary
|
||||||
|
working-directory: signal
|
||||||
|
run: CGO_ENABLED=0 go build -o netbird-signal main.go
|
||||||
|
|
||||||
|
- name: Build signal docker image
|
||||||
|
working-directory: signal
|
||||||
|
run: |
|
||||||
|
docker build -t netbirdio/signal:latest .
|
||||||
|
|
||||||
- name: run docker compose up
|
- name: run docker compose up
|
||||||
working-directory: infrastructure_files
|
working-directory: infrastructure_files
|
||||||
run: |
|
run: |
|
||||||
@@ -120,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
- name: test running containers
|
- name: test running containers
|
||||||
run: |
|
run: |
|
||||||
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
count=$(docker compose ps --format json | jq '. | select(.Name | contains("infrastructure_files")) | .State' | grep -c running)
|
||||||
test $count -eq 4
|
test $count -eq 4
|
||||||
working-directory: infrastructure_files
|
working-directory: infrastructure_files
|
||||||
|
|
||||||
|
|||||||
@@ -84,10 +84,14 @@ func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
|
|||||||
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
||||||
supportsSSO := true
|
supportsSSO := true
|
||||||
err := a.withBackOff(a.ctx, func() (err error) {
|
err := a.withBackOff(a.ctx, func() (err error) {
|
||||||
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
_, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
||||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.NotFound || s.Code() == codes.Unimplemented) {
|
||||||
_, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
||||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
|
s, ok := gstatus.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.Code() == codes.NotFound || s.Code() == codes.Unimplemented {
|
||||||
supportsSSO = false
|
supportsSSO = false
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,7 @@ func (a *Auth) login(urlOpener URLOpener) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
|
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config)
|
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -82,9 +81,10 @@ var loginCmd = &cobra.Command{
|
|||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: setupKey,
|
SetupKey: setupKey,
|
||||||
PreSharedKey: preSharedKey,
|
PreSharedKey: preSharedKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
|
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
@@ -165,7 +165,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isLinuxRunningDesktop())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -195,60 +195,17 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
|||||||
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
browserAuthMsg := "Please do the SSO login in your browser. \n" +
|
cmd.Println("Please do the SSO login in your browser. \n" +
|
||||||
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
||||||
verificationURIComplete + " " + codeMsg
|
verificationURIComplete + " " + codeMsg)
|
||||||
|
cmd.Println("")
|
||||||
setupKeyAuthMsg := "\nAlternatively, you may want to use a setup key, see:\n\n" +
|
if err := open.Run(verificationURIComplete); err != nil {
|
||||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
cmd.Println("\nAlternatively, you may want to use a setup key, see:\n\n" +
|
||||||
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
authenticateUsingBrowser := func() {
|
|
||||||
cmd.Println(browserAuthMsg)
|
|
||||||
cmd.Println("")
|
|
||||||
if err := open.Run(verificationURIComplete); err != nil {
|
|
||||||
cmd.Println(setupKeyAuthMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows", "darwin":
|
|
||||||
authenticateUsingBrowser()
|
|
||||||
case "linux":
|
|
||||||
if isLinuxRunningDesktop() {
|
|
||||||
authenticateUsingBrowser()
|
|
||||||
} else {
|
|
||||||
// If current flow is PKCE, it implies the server is anticipating the redirect to localhost.
|
|
||||||
// Devices lacking browser support are incompatible with this flow.Therefore,
|
|
||||||
// these devices will need to resort to setup keys instead.
|
|
||||||
if isPKCEFlow(verificationURIComplete) {
|
|
||||||
cmd.Println("Please proceed with setting up this device using setup keys, see:\n\n" +
|
|
||||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
|
||||||
} else {
|
|
||||||
cmd.Println(browserAuthMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment.
|
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment
|
||||||
func isLinuxRunningDesktop() bool {
|
func isLinuxRunningDesktop() bool {
|
||||||
for _, env := range os.Environ() {
|
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
|
||||||
values := strings.Split(env, "=")
|
|
||||||
if len(values) == 2 {
|
|
||||||
key, value := values[0], values[1]
|
|
||||||
if key == "XDG_CURRENT_DESKTOP" && value != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPKCEFlow determines if the PKCE flow is active or not,
|
|
||||||
// by checking the existence of redirect_uri inside the verification URL.
|
|
||||||
func isPKCEFlow(verificationURL string) bool {
|
|
||||||
if verificationURL == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(verificationURL, "redirect_uri")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||||
eventStore)
|
eventStore, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,13 +141,14 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: setupKey,
|
SetupKey: setupKey,
|
||||||
PreSharedKey: preSharedKey,
|
PreSharedKey: preSharedKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
AdminURL: adminURL,
|
AdminURL: adminURL,
|
||||||
NatExternalIPs: natExternalIPs,
|
NatExternalIPs: natExternalIPs,
|
||||||
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
||||||
CustomDNSAddress: customDNSAddressConverted,
|
CustomDNSAddress: customDNSAddressConverted,
|
||||||
|
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func Create(wgIface iFaceMapper, ipv6Supported bool) (*Manager, error) {
|
|||||||
|
|
||||||
// AddFiltering rule to the firewall
|
// AddFiltering rule to the firewall
|
||||||
//
|
//
|
||||||
// If comment is empty rule ID is used as comment
|
// Comment will be ignored because some system this feature is not supported
|
||||||
func (m *Manager) AddFiltering(
|
func (m *Manager) AddFiltering(
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
protocol fw.Protocol,
|
protocol fw.Protocol,
|
||||||
@@ -123,9 +123,6 @@ func (m *Manager) AddFiltering(
|
|||||||
ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal)
|
ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal)
|
||||||
|
|
||||||
ruleID := uuid.New().String()
|
ruleID := uuid.New().String()
|
||||||
if comment == "" {
|
|
||||||
comment = ruleID
|
|
||||||
}
|
|
||||||
|
|
||||||
if ipsetName != "" {
|
if ipsetName != "" {
|
||||||
rs, rsExists := m.rulesets[ipsetName]
|
rs, rsExists := m.rulesets[ipsetName]
|
||||||
@@ -157,8 +154,7 @@ func (m *Manager) AddFiltering(
|
|||||||
// this is new ipset so we need to create firewall rule for it
|
// this is new ipset so we need to create firewall rule for it
|
||||||
}
|
}
|
||||||
|
|
||||||
specs := m.filterRuleSpecs("filter", ip, string(protocol), sPortVal, dPortVal,
|
specs := m.filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, direction, action, ipsetName)
|
||||||
direction, action, comment, ipsetName)
|
|
||||||
|
|
||||||
if direction == fw.RuleDirectionOUT {
|
if direction == fw.RuleDirectionOUT {
|
||||||
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
|
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
|
||||||
@@ -283,7 +279,7 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
fw.RuleDirectionIN,
|
fw.RuleDirectionIN,
|
||||||
fw.ActionAccept,
|
fw.ActionAccept,
|
||||||
"",
|
"",
|
||||||
"allow netbird interface traffic",
|
"",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to allow netbird interface traffic: %w", err)
|
return fmt.Errorf("failed to allow netbird interface traffic: %w", err)
|
||||||
@@ -296,7 +292,7 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
fw.RuleDirectionOUT,
|
fw.RuleDirectionOUT,
|
||||||
fw.ActionAccept,
|
fw.ActionAccept,
|
||||||
"",
|
"",
|
||||||
"allow netbird interface traffic",
|
"",
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -362,9 +358,7 @@ func (m *Manager) reset(client *iptables.IPTables, table string) error {
|
|||||||
|
|
||||||
// filterRuleSpecs returns the specs of a filtering rule
|
// filterRuleSpecs returns the specs of a filtering rule
|
||||||
func (m *Manager) filterRuleSpecs(
|
func (m *Manager) filterRuleSpecs(
|
||||||
table string, ip net.IP, protocol string, sPort, dPort string,
|
ip net.IP, protocol string, sPort, dPort string, direction fw.RuleDirection, action fw.Action, ipsetName string,
|
||||||
direction fw.RuleDirection, action fw.Action, comment string,
|
|
||||||
ipsetName string,
|
|
||||||
) (specs []string) {
|
) (specs []string) {
|
||||||
matchByIP := true
|
matchByIP := true
|
||||||
// don't use IP matching if IP is ip 0.0.0.0
|
// don't use IP matching if IP is ip 0.0.0.0
|
||||||
@@ -398,8 +392,7 @@ func (m *Manager) filterRuleSpecs(
|
|||||||
if dPort != "" {
|
if dPort != "" {
|
||||||
specs = append(specs, "--dport", dPort)
|
specs = append(specs, "--dport", dPort)
|
||||||
}
|
}
|
||||||
specs = append(specs, "-j", m.actionToStr(action))
|
return append(specs, "-j", m.actionToStr(action))
|
||||||
return append(specs, "-m", "comment", "--comment", comment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rawClient returns corresponding iptables client for the given ip
|
// rawClient returns corresponding iptables client for the given ip
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux || android
|
||||||
|
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -57,25 +58,45 @@ func (t TokenInfo) GetTokenToUse() string {
|
|||||||
return t.AccessToken
|
return t.AccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
|
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration
|
||||||
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
//
|
||||||
log.Debug("loading pkce authorization flow info")
|
// It starts by initializing the PKCE.If this process fails, it resorts to the Device Code Flow,
|
||||||
|
// and if that also fails, the authentication process is deemed unsuccessful
|
||||||
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
//
|
||||||
if err == nil {
|
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
|
||||||
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopClient bool) (OAuthFlow, error) {
|
||||||
|
if runtime.GOOS == "linux" && !isLinuxDesktopClient {
|
||||||
|
return authenticateWithDeviceCodeFlow(ctx, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("loading pkce authorization flow info failed with error: %v", err)
|
pkceFlow, err := authenticateWithPKCEFlow(ctx, config)
|
||||||
log.Debugf("falling back to device authorization flow info")
|
if err != nil {
|
||||||
|
// fallback to device code flow
|
||||||
|
log.Debugf("failed to initialize pkce authentication with error: %v\n", err)
|
||||||
|
log.Debug("falling back to device code flow")
|
||||||
|
return authenticateWithDeviceCodeFlow(ctx, config)
|
||||||
|
}
|
||||||
|
return pkceFlow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateWithPKCEFlow initializes the Proof Key for Code Exchange flow auth flow
|
||||||
|
func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
||||||
|
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
|
||||||
|
}
|
||||||
|
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateWithDeviceCodeFlow initializes the Device Code auth Flow
|
||||||
|
func authenticateWithDeviceCodeFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
||||||
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s, ok := gstatus.FromError(err)
|
s, ok := gstatus.FromError(err)
|
||||||
if ok && s.Code() == codes.NotFound {
|
if ok && s.Code() == codes.NotFound {
|
||||||
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
||||||
"If you are using hosting Netbird see documentation at " +
|
"Please proceed with setting up this device using setup keys " +
|
||||||
"https://github.com/netbirdio/netbird/tree/main/management for details")
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
} else if ok && s.Code() == codes.Unimplemented {
|
} else if ok && s.Code() == codes.Unimplemented {
|
||||||
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
||||||
"please update your server or use Setup Keys to login", config.ManagementURL)
|
"please update your server or use Setup Keys to login", config.ManagementURL)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -80,7 +79,7 @@ func (p *PKCEAuthorizationFlow) GetClientID(_ context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestAuthInfo requests a authorization code login flow information.
|
// RequestAuthInfo requests a authorization code login flow information.
|
||||||
func (p *PKCEAuthorizationFlow) RequestAuthInfo(_ context.Context) (AuthFlowInfo, error) {
|
func (p *PKCEAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error) {
|
||||||
state, err := randomBytesInHex(24)
|
state, err := randomBytesInHex(24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err)
|
return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err)
|
||||||
@@ -114,64 +113,37 @@ func (p *PKCEAuthorizationFlow) WaitToken(ctx context.Context, _ AuthFlowInfo) (
|
|||||||
tokenChan := make(chan *oauth2.Token, 1)
|
tokenChan := make(chan *oauth2.Token, 1)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
go p.startServer(tokenChan, errChan)
|
parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return TokenInfo{}, fmt.Errorf("failed to parse redirect URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{Addr: fmt.Sprintf(":%s", parsedURL.Port())}
|
||||||
|
defer func() {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Errorf("failed to close the server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go p.startServer(server, tokenChan, errChan)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return TokenInfo{}, ctx.Err()
|
return TokenInfo{}, ctx.Err()
|
||||||
case token := <-tokenChan:
|
case token := <-tokenChan:
|
||||||
return p.handleOAuthToken(token)
|
return p.parseOAuthToken(token)
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
return TokenInfo{}, err
|
return TokenInfo{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errChan chan<- error) {
|
func (p *PKCEAuthorizationFlow) startServer(server *http.Server, tokenChan chan<- *oauth2.Token, errChan chan<- error) {
|
||||||
var wg sync.WaitGroup
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL)
|
token, err := p.handleRequest(req)
|
||||||
if err != nil {
|
|
||||||
errChan <- fmt.Errorf("failed to parse redirect URL: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
server := http.Server{Addr: fmt.Sprintf(":%s", parsedURL.Port())}
|
|
||||||
go func() {
|
|
||||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
|
||||||
query := req.URL.Query()
|
|
||||||
|
|
||||||
if authError := query.Get(queryError); authError != "" {
|
|
||||||
authErrorDesc := query.Get(queryErrorDesc)
|
|
||||||
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent timing attacks on state
|
|
||||||
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid state")
|
|
||||||
}
|
|
||||||
|
|
||||||
code := query.Get(queryCode)
|
|
||||||
if code == "" {
|
|
||||||
return nil, fmt.Errorf("missing code")
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.oAuthConfig.Exchange(
|
|
||||||
req.Context(),
|
|
||||||
code,
|
|
||||||
oauth2.SetAuthURLParam("code_verifier", p.codeVerifier),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := tokenValidatorFunc()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
renderPKCEFlowTmpl(w, err)
|
renderPKCEFlowTmpl(w, err)
|
||||||
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
||||||
@@ -182,13 +154,38 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
tokenChan <- token
|
tokenChan <- token
|
||||||
})
|
})
|
||||||
|
|
||||||
wg.Wait()
|
server.Handler = mux
|
||||||
if err := server.Shutdown(context.Background()); err != nil {
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Errorf("error while shutting down pkce flow server: %v", err)
|
errChan <- err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo, error) {
|
func (p *PKCEAuthorizationFlow) handleRequest(req *http.Request) (*oauth2.Token, error) {
|
||||||
|
query := req.URL.Query()
|
||||||
|
|
||||||
|
if authError := query.Get(queryError); authError != "" {
|
||||||
|
authErrorDesc := query.Get(queryErrorDesc)
|
||||||
|
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent timing attacks on the state
|
||||||
|
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid state")
|
||||||
|
}
|
||||||
|
|
||||||
|
code := query.Get(queryCode)
|
||||||
|
if code == "" {
|
||||||
|
return nil, fmt.Errorf("missing code")
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.oAuthConfig.Exchange(
|
||||||
|
req.Context(),
|
||||||
|
code,
|
||||||
|
oauth2.SetAuthURLParam("code_verifier", p.codeVerifier),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo, error) {
|
||||||
tokenInfo := TokenInfo{
|
tokenInfo := TokenInfo{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
RefreshToken: token.RefreshToken,
|
RefreshToken: token.RefreshToken,
|
||||||
@@ -200,7 +197,13 @@ func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo
|
|||||||
tokenInfo.IDToken = idToken
|
tokenInfo.IDToken = idToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil {
|
// if a provider doesn't support an audience, use the Client ID for token verification
|
||||||
|
audience := p.providerConfig.Audience
|
||||||
|
if audience == "" {
|
||||||
|
audience = p.providerConfig.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), audience); err != nil {
|
||||||
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,15 +43,14 @@ func isValidAccessToken(token string, audience string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audience claim of JWT can be a string or an array of strings
|
// Audience claim of JWT can be a string or an array of strings
|
||||||
typ := reflect.TypeOf(claims.Audience)
|
switch aud := claims.Audience.(type) {
|
||||||
switch typ.Kind() {
|
case string:
|
||||||
case reflect.String:
|
if aud == audience {
|
||||||
if claims.Audience == audience {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case reflect.Slice:
|
case []interface{}:
|
||||||
for _, aud := range claims.Audience.([]interface{}) {
|
for _, audItem := range aud {
|
||||||
if audience == aud {
|
if audStr, ok := audItem.(string); ok && audStr == audience {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
//go:build !linux
|
//go:build !linux || android
|
||||||
|
|
||||||
package checkfw
|
package checkfw
|
||||||
|
|||||||
@@ -1049,7 +1049,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
|
|||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||||
eventStore)
|
eventStore, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,9 +106,6 @@ func GetPKCEAuthorizationFlowInfo(ctx context.Context, privateKey string, mgmURL
|
|||||||
|
|
||||||
func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error {
|
func isPKCEProviderConfigValid(config PKCEAuthProviderConfig) error {
|
||||||
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
||||||
if config.Audience == "" {
|
|
||||||
return fmt.Errorf(errorMSGFormat, "Audience")
|
|
||||||
}
|
|
||||||
if config.ClientID == "" {
|
if config.ClientID == "" {
|
||||||
return fmt.Errorf(errorMSGFormat, "Client ID")
|
return fmt.Errorf(errorMSGFormat, "Client ID")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ func (p *WGEBPFProxy) proxyToLocal(endpointPort uint16, remoteConn net.Conn) {
|
|||||||
log.Errorf("failed to read from turn conn (endpoint: :%d): %s", endpointPort, err)
|
log.Errorf("failed to read from turn conn (endpoint: :%d): %s", endpointPort, err)
|
||||||
}
|
}
|
||||||
p.removeTurnConn(endpointPort)
|
p.removeTurnConn(endpointPort)
|
||||||
|
log.Infof("stop forward turn packages to port: %d. error: %s", endpointPort, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = p.sendPkg(buf[:n], endpointPort)
|
err = p.sendPkg(buf[:n], endpointPort)
|
||||||
@@ -158,7 +159,7 @@ func (p *WGEBPFProxy) proxyToRemote() {
|
|||||||
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
conn, ok := p.turnConnStore[uint16(addr.Port)]
|
||||||
p.turnConnMutex.Unlock()
|
p.turnConnMutex.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Errorf("turn conn not found by port: %d", addr.Port)
|
log.Infof("turn conn not found by port: %d", addr.Port)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// protoc-gen-go v1.26.0
|
// protoc-gen-go v1.26.0
|
||||||
// protoc v3.21.9
|
// protoc v4.23.4
|
||||||
// source: daemon.proto
|
// source: daemon.proto
|
||||||
|
|
||||||
package proto
|
package proto
|
||||||
@@ -40,8 +40,9 @@ type LoginRequest struct {
|
|||||||
// cleanNATExternalIPs clean map list of external IPs.
|
// cleanNATExternalIPs clean map list of external IPs.
|
||||||
// This is needed because the generated code
|
// This is needed because the generated code
|
||||||
// omits initialized empty slices due to omitempty tags
|
// omits initialized empty slices due to omitempty tags
|
||||||
CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"`
|
CleanNATExternalIPs bool `protobuf:"varint,6,opt,name=cleanNATExternalIPs,proto3" json:"cleanNATExternalIPs,omitempty"`
|
||||||
CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"`
|
CustomDNSAddress []byte `protobuf:"bytes,7,opt,name=customDNSAddress,proto3" json:"customDNSAddress,omitempty"`
|
||||||
|
IsLinuxDesktopClient bool `protobuf:"varint,8,opt,name=isLinuxDesktopClient,proto3" json:"isLinuxDesktopClient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *LoginRequest) Reset() {
|
func (x *LoginRequest) Reset() {
|
||||||
@@ -125,6 +126,13 @@ func (x *LoginRequest) GetCustomDNSAddress() []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetIsLinuxDesktopClient() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.IsLinuxDesktopClient
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
state protoimpl.MessageState
|
state protoimpl.MessageState
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
@@ -1043,7 +1051,7 @@ var file_daemon_proto_rawDesc = []byte{
|
|||||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
|
||||||
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x96, 0x02, 0x0a, 0x0c, 0x4c, 0x6f,
|
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xca, 0x02, 0x0a, 0x0c, 0x4c, 0x6f,
|
||||||
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
|
0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65,
|
||||||
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
|
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65,
|
||||||
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
|
0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61,
|
||||||
@@ -1061,128 +1069,131 @@ var file_daemon_proto_rawDesc = []byte{
|
|||||||
0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
|
0x6e, 0x61, 0x6c, 0x49, 0x50, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
|
||||||
0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c,
|
0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c,
|
||||||
0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65,
|
0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x44, 0x4e, 0x53, 0x41, 0x64, 0x64, 0x72, 0x65,
|
||||||
0x73, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
|
0x73, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73,
|
||||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f,
|
0x6b, 0x74, 0x6f, 0x70, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x65, 0x65,
|
0x52, 0x14, 0x69, 0x73, 0x4c, 0x69, 0x6e, 0x75, 0x78, 0x44, 0x65, 0x73, 0x6b, 0x74, 0x6f, 0x70,
|
||||||
0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
|
0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
|
||||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6e, 0x65, 0x65, 0x64,
|
||||||
0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69,
|
0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
0x0d, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1a,
|
||||||
0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49,
|
0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x0f, 0x76, 0x65,
|
||||||
0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
|
0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x18, 0x03, 0x20,
|
||||||
0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x55,
|
0x01, 0x28, 0x09, 0x52, 0x0f, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||||
0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x31, 0x0a, 0x13, 0x57, 0x61,
|
0x6e, 0x55, 0x52, 0x49, 0x12, 0x38, 0x0a, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61,
|
||||||
0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x74, 0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18,
|
||||||
0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20,
|
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x76, 0x65, 0x72, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x22, 0x16, 0x0a,
|
0x69, 0x6f, 0x6e, 0x55, 0x52, 0x49, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x31,
|
||||||
0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73,
|
0x0a, 0x13, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
|
||||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64,
|
||||||
0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x64,
|
||||||
0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x65, 0x22, 0x16, 0x0a, 0x14, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69,
|
||||||
0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72,
|
0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x55, 0x70, 0x52,
|
||||||
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x67, 0x65,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0c, 0x0a, 0x0a, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22,
|
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
|
||||||
0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c,
|
||||||
0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01,
|
0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32, 0x0a, 0x0a, 0x66, 0x75,
|
0x52, 0x11, 0x67, 0x65, 0x74, 0x46, 0x75, 0x6c, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
|
||||||
0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
|
0x74, 0x75, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65,
|
||||||
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74,
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
|
||||||
0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x32,
|
||||||
0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
|
0x0a, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72,
|
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x46, 0x75, 0x6c, 0x6c,
|
||||||
0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75,
|
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x66, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74,
|
||||||
0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x75, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x56, 0x65, 0x72, 0x73,
|
||||||
0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
||||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x43,
|
0x6e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x0d, 0x0a, 0x0b, 0x44, 0x6f, 0x77, 0x6e,
|
||||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a,
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x0e, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x52,
|
||||||
0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x18, 0x01,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x6f,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xb3, 0x01, 0x0a, 0x11,
|
||||||
0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46, 0x69, 0x6c,
|
0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x46,
|
0x65, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x55,
|
||||||
0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x03,
|
0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x22, 0x0a,
|
0x6d, 0x65, 0x6e, 0x74, 0x55, 0x72, 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69,
|
||||||
0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x18, 0x04, 0x20,
|
0x67, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
|
0x66, 0x69, 0x67, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69,
|
||||||
0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20,
|
0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6c, 0x6f, 0x67, 0x46, 0x69, 0x6c,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52, 0x4c, 0x22, 0xcf, 0x02,
|
0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65,
|
||||||
0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49,
|
0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x53, 0x68, 0x61, 0x72,
|
||||||
0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70,
|
0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52,
|
||||||
0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62,
|
0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x52,
|
||||||
0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75,
|
0x4c, 0x22, 0xcf, 0x02, 0x0a, 0x09, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
|
||||||
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61,
|
0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12,
|
||||||
0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75,
|
0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
|
0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x53,
|
||||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e,
|
||||||
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53,
|
0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x46, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x6e, 0x53,
|
||||||
0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72,
|
0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||||
0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65,
|
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18,
|
0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63,
|
||||||
0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, 0x34, 0x0a,
|
0x6f, 0x6e, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12,
|
||||||
0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61,
|
0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
|
||||||
0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x6c, 0x6f,
|
0x52, 0x07, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x72,
|
||||||
0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54,
|
0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63,
|
||||||
0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65,
|
0x74, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e,
|
||||||
0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20,
|
0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x63, 0x65, 0x43, 0x61,
|
0x52, 0x15, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64,
|
||||||
0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66,
|
0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74,
|
||||||
0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22,
|
0x65, 0x49, 0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70,
|
||||||
0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
|
0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49,
|
||||||
0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49,
|
0x63, 0x65, 0x43, 0x61, 0x6e, 0x64, 0x69, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||||
0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
|
0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66,
|
||||||
0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x6b, 0x65, 0x72,
|
0x71, 0x64, 0x6e, 0x22, 0x76, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72,
|
||||||
0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01,
|
0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66,
|
0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18,
|
||||||
0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28,
|
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x28, 0x0a,
|
||||||
0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x61,
|
0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65,
|
||||||
0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20,
|
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x49, 0x6e,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e,
|
0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x18,
|
||||||
0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e,
|
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x71, 0x64, 0x6e, 0x22, 0x3d, 0x0a, 0x0b, 0x53,
|
||||||
0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65,
|
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52,
|
||||||
0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c,
|
0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09,
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x1c, 0x0a, 0x09, 0x63,
|
0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||||
0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09,
|
0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0x41, 0x0a, 0x0f, 0x4d, 0x61,
|
||||||
0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x0a, 0x46, 0x75,
|
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x10, 0x0a,
|
||||||
0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f, 0x6d, 0x61, 0x6e, 0x61,
|
0x03, 0x55, 0x52, 0x4c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12,
|
||||||
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01,
|
||||||
0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67,
|
0x28, 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x64, 0x22, 0xef, 0x01,
|
||||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f, 0x6d, 0x61, 0x6e, 0x61,
|
0x0a, 0x0a, 0x46, 0x75, 0x6c, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x41, 0x0a, 0x0f,
|
||||||
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x0b, 0x73,
|
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18,
|
||||||
0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d,
|
||||||
0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x6c,
|
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0f,
|
||||||
0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61,
|
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12,
|
||||||
0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53,
|
0x35, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02,
|
||||||
0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x64, 0x61, 0x65,
|
0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x69,
|
||||||
0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
|
0x67, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x61,
|
||||||
0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65, 0x72, 0x53, 0x74, 0x61,
|
0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50,
|
||||||
0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28,
|
0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16,
|
||||||
0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x53,
|
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65,
|
||||||
0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32, 0xf7, 0x02, 0x0a, 0x0d,
|
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x65, 0x65,
|
||||||
0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a,
|
0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18,
|
||||||
0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
|
0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x50,
|
||||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64,
|
0x65, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x32,
|
||||||
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0xf7, 0x02, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||||
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f,
|
0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65,
|
||||||
0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57,
|
0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||||
0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
|
||||||
0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74,
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69,
|
||||||
0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d,
|
||||||
0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52,
|
||||||
0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
|
||||||
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70,
|
||||||
0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61,
|
0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64,
|
||||||
0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
|
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74,
|
0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04,
|
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
|
||||||
0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f,
|
0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
|
||||||
0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d,
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e,
|
||||||
0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22,
|
0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
|
||||||
0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18,
|
0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
||||||
0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e,
|
||||||
0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f,
|
0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||||
0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66,
|
||||||
0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43,
|
||||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64,
|
||||||
|
0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||||
|
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ message LoginRequest {
|
|||||||
|
|
||||||
bytes customDNSAddress = 7;
|
bytes customDNSAddress = 7;
|
||||||
|
|
||||||
|
bool isLinuxDesktopClient = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginResponse {
|
message LoginResponse {
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
|
|||||||
state.Set(internal.StatusConnecting)
|
state.Set(internal.StatusConnecting)
|
||||||
|
|
||||||
if msg.SetupKey == "" {
|
if msg.SetupKey == "" {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, msg.IsLinuxDesktopClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state.Set(internal.StatusLoginFailed)
|
state.Set(internal.StatusLoginFailed)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
5
client/ui/build-ui-linux.sh
Normal file
5
client/ui/build-ui-linux.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt remove gir1.2-appindicator3-0.1
|
||||||
|
sudo apt install -y libayatana-appindicator3-dev
|
||||||
|
go build
|
||||||
@@ -202,9 +202,10 @@ func (s *serviceClient) getSettingsForm() *widget.Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.Login(s.ctx, &proto.LoginRequest{
|
_, err = client.Login(s.ctx, &proto.LoginRequest{
|
||||||
ManagementUrl: s.iMngURL.Text,
|
ManagementUrl: s.iMngURL.Text,
|
||||||
AdminURL: s.iAdminURL.Text,
|
AdminURL: s.iAdminURL.Text,
|
||||||
PreSharedKey: s.iPreSharedKey.Text,
|
PreSharedKey: s.iPreSharedKey.Text,
|
||||||
|
IsLinuxDesktopClient: runtime.GOOS == "linux",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("login to management URL: %v", err)
|
log.Errorf("login to management URL: %v", err)
|
||||||
@@ -233,7 +234,9 @@ func (s *serviceClient) login() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{})
|
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{
|
||||||
|
IsLinuxDesktopClient: runtime.GOOS == "linux",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("login to management URL with: %v", err)
|
log.Errorf("login to management URL with: %v", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -29,6 +29,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.1.4
|
fyne.io/fyne/v2 v2.1.4
|
||||||
|
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
|
||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/cilium/ebpf v0.10.0
|
github.com/cilium/ebpf v0.10.0
|
||||||
github.com/coreos/go-iptables v0.7.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -61,6 +61,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
|||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
|
||||||
|
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
|
||||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10=
|
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10=
|
||||||
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY=
|
github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY=
|
||||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
|||||||
# PKCE authorization flow
|
# PKCE authorization flow
|
||||||
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
|
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS=${NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS:-"53000"}
|
||||||
NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
|
NETBIRD_AUTH_PKCE_USE_ID_TOKEN=${NETBIRD_AUTH_PKCE_USE_ID_TOKEN:-false}
|
||||||
|
NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
|
||||||
|
# The default setting is to transmit the audience to the IDP during authorization. However,
|
||||||
|
# if your IDP does not have this capability, you can turn this off by setting it to false.
|
||||||
|
NETBIRD_DASH_AUTH_USE_AUDIENCE=${NETBIRD_DASH_AUTH_USE_AUDIENCE:-true}
|
||||||
|
NETBIRD_DASH_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
||||||
|
|
||||||
# exports
|
# exports
|
||||||
export NETBIRD_DOMAIN
|
export NETBIRD_DOMAIN
|
||||||
@@ -86,4 +94,7 @@ export NETBIRD_TOKEN_SOURCE
|
|||||||
export NETBIRD_AUTH_DEVICE_AUTH_SCOPE
|
export NETBIRD_AUTH_DEVICE_AUTH_SCOPE
|
||||||
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
|
export NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN
|
||||||
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
|
export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
|
||||||
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
|
export NETBIRD_AUTH_PKCE_USE_ID_TOKEN
|
||||||
|
export NETBIRD_AUTH_PKCE_AUDIENCE
|
||||||
|
export NETBIRD_DASH_AUTH_USE_AUDIENCE
|
||||||
|
export NETBIRD_DASH_AUTH_AUDIENCE
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
if ! which curl >/dev/null 2>&1; then
|
if ! which curl >/dev/null 2>&1; then
|
||||||
echo "This script uses curl fetch OpenID configuration from IDP."
|
echo "This script uses curl fetch OpenID configuration from IDP."
|
||||||
@@ -154,6 +155,8 @@ if [ -n "$NETBIRD_MGMT_IDP" ]; then
|
|||||||
export NETBIRD_IDP_MGMT_CLIENT_ID
|
export NETBIRD_IDP_MGMT_CLIENT_ID
|
||||||
export NETBIRD_IDP_MGMT_CLIENT_SECRET
|
export NETBIRD_IDP_MGMT_CLIENT_SECRET
|
||||||
export NETBIRD_IDP_MGMT_EXTRA_CONFIG=$EXTRA_CONFIG
|
export NETBIRD_IDP_MGMT_EXTRA_CONFIG=$EXTRA_CONFIG
|
||||||
|
else
|
||||||
|
export NETBIRD_IDP_MGMT_EXTRA_CONFIG={}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IFS=',' read -r -a REDIRECT_URL_PORTS <<< "$NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS"
|
IFS=',' read -r -a REDIRECT_URL_PORTS <<< "$NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS"
|
||||||
@@ -164,8 +167,35 @@ done
|
|||||||
|
|
||||||
export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,}
|
export NETBIRD_AUTH_PKCE_REDIRECT_URLS=${REDIRECT_URLS%,}
|
||||||
|
|
||||||
|
# Remove audience for providers that do not support it
|
||||||
|
if [ "$NETBIRD_DASH_AUTH_USE_AUDIENCE" = "false" ]; then
|
||||||
|
export NETBIRD_DASH_AUTH_AUDIENCE=none
|
||||||
|
export NETBIRD_AUTH_PKCE_AUDIENCE=
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read the encryption key
|
||||||
|
if test -f 'management.json'; then
|
||||||
|
encKey=$(jq -r ".DataStoreEncryptionKey" management.json)
|
||||||
|
if [[ "$encKey" != "null" ]]; then
|
||||||
|
export NETBIRD_DATASTORE_ENC_KEY=$encKey
|
||||||
|
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
env | grep NETBIRD
|
env | grep NETBIRD
|
||||||
|
|
||||||
|
bkp_postfix="$(date +%s)"
|
||||||
|
if test -f 'docker-compose.yml'; then
|
||||||
|
cp docker-compose.yml "docker-compose.yml.bkp.${bkp_postfix}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -f 'management.json'; then
|
||||||
|
cp management.json "management.json.bkp.${bkp_postfix}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -f 'turnserver.conf'; then
|
||||||
|
cp turnserver.conf "turnserver.conf.bpk.${bkp_postfix}"
|
||||||
|
fi
|
||||||
envsubst <docker-compose.yml.tmpl >docker-compose.yml
|
envsubst <docker-compose.yml.tmpl >docker-compose.yml
|
||||||
envsubst <management.json.tmpl >management.json
|
envsubst <management.json.tmpl | jq . >management.json
|
||||||
envsubst <turnserver.conf.tmpl >turnserver.conf
|
envsubst <turnserver.conf.tmpl >turnserver.conf
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||||
# OIDC
|
# OIDC
|
||||||
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
|
||||||
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||||
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
||||||
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- $SIGNAL_VOLUMENAME:/var/lib/netbird
|
- $SIGNAL_VOLUMENAME:/var/lib/netbird
|
||||||
ports:
|
ports:
|
||||||
- 10000:80
|
- $NETBIRD_SIGNAL_PORT:80
|
||||||
# # port and command for Let's Encrypt validation
|
# # port and command for Let's Encrypt validation
|
||||||
# - 443:443
|
# - 443:443
|
||||||
# command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"]
|
# command: ["--letsencrypt-domain", "$NETBIRD_LETSENCRYPT_DOMAIN", "--log-file", "console"]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
- NETBIRD_MGMT_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||||
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
- NETBIRD_MGMT_GRPC_API_ENDPOINT=$NETBIRD_MGMT_API_ENDPOINT
|
||||||
# OIDC
|
# OIDC
|
||||||
- AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
|
- AUTH_AUDIENCE=$NETBIRD_DASH_AUTH_AUDIENCE
|
||||||
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
- AUTH_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
|
||||||
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
- AUTH_CLIENT_SECRET=$NETBIRD_AUTH_CLIENT_SECRET
|
||||||
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
- AUTH_AUTHORITY=$NETBIRD_AUTH_AUTHORITY
|
||||||
@@ -20,6 +20,7 @@ services:
|
|||||||
- AUTH_SUPPORTED_SCOPES=$NETBIRD_AUTH_SUPPORTED_SCOPES
|
- AUTH_SUPPORTED_SCOPES=$NETBIRD_AUTH_SUPPORTED_SCOPES
|
||||||
- AUTH_REDIRECT_URI=$NETBIRD_AUTH_REDIRECT_URI
|
- AUTH_REDIRECT_URI=$NETBIRD_AUTH_REDIRECT_URI
|
||||||
- AUTH_SILENT_REDIRECT_URI=$NETBIRD_AUTH_SILENT_REDIRECT_URI
|
- AUTH_SILENT_REDIRECT_URI=$NETBIRD_AUTH_SILENT_REDIRECT_URI
|
||||||
|
- NETBIRD_TOKEN_SOURCE=$NETBIRD_TOKEN_SOURCE
|
||||||
# SSL
|
# SSL
|
||||||
- NGINX_SSL_PORT=443
|
- NGINX_SSL_PORT=443
|
||||||
# Letsencrypt
|
# Letsencrypt
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"Password": null
|
"Password": null
|
||||||
},
|
},
|
||||||
"Datadir": "",
|
"Datadir": "",
|
||||||
|
"DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY",
|
||||||
"HttpConfig": {
|
"HttpConfig": {
|
||||||
"Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT",
|
"Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT",
|
||||||
"AuthIssuer": "$NETBIRD_AUTH_AUTHORITY",
|
"AuthIssuer": "$NETBIRD_AUTH_AUTHORITY",
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
"PKCEAuthorizationFlow": {
|
"PKCEAuthorizationFlow": {
|
||||||
"ProviderConfig": {
|
"ProviderConfig": {
|
||||||
"Audience": "$NETBIRD_AUTH_AUDIENCE",
|
"Audience": "$NETBIRD_AUTH_PKCE_AUDIENCE",
|
||||||
"ClientID": "$NETBIRD_AUTH_CLIENT_ID",
|
"ClientID": "$NETBIRD_AUTH_CLIENT_ID",
|
||||||
"ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET",
|
"ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET",
|
||||||
"AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT",
|
"AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT",
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ NETBIRD_DOMAIN=""
|
|||||||
# e.g., https://example.eu.auth0.com/.well-known/openid-configuration
|
# e.g., https://example.eu.auth0.com/.well-known/openid-configuration
|
||||||
# -------------------------------------------
|
# -------------------------------------------
|
||||||
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
|
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT=""
|
||||||
|
# The default setting is to transmit the audience to the IDP during authorization. However,
|
||||||
|
# if your IDP does not have this capability, you can turn this off by setting it to false.
|
||||||
|
#NETBIRD_DASH_AUTH_USE_AUDIENCE=false
|
||||||
NETBIRD_AUTH_AUDIENCE=""
|
NETBIRD_AUTH_AUDIENCE=""
|
||||||
# e.g. netbird-client
|
# e.g. netbird-client
|
||||||
NETBIRD_AUTH_CLIENT_ID=""
|
NETBIRD_AUTH_CLIENT_ID=""
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ NETBIRD_AUTH_USER_ID_CLAIM="email"
|
|||||||
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid email"
|
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid email"
|
||||||
NETBIRD_MGMT_IDP=$CI_NETBIRD_MGMT_IDP
|
NETBIRD_MGMT_IDP=$CI_NETBIRD_MGMT_IDP
|
||||||
NETBIRD_IDP_MGMT_CLIENT_ID=$CI_NETBIRD_IDP_MGMT_CLIENT_ID
|
NETBIRD_IDP_MGMT_CLIENT_ID=$CI_NETBIRD_IDP_MGMT_CLIENT_ID
|
||||||
NETBIRD_IDP_MGMT_CLIENT_SECRET=$CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
|
NETBIRD_IDP_MGMT_CLIENT_SECRET=$CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
|
||||||
|
NETBIRD_SIGNAL_PORT=12345
|
||||||
@@ -61,7 +61,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||||
eventStore)
|
eventStore, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/encryption"
|
"github.com/netbirdio/netbird/encryption"
|
||||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
"github.com/netbirdio/netbird/management/server"
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/activity/sqlite"
|
"github.com/netbirdio/netbird/management/server/activity/sqlite"
|
||||||
httpapi "github.com/netbirdio/netbird/management/server/http"
|
httpapi "github.com/netbirdio/netbird/management/server/http"
|
||||||
"github.com/netbirdio/netbird/management/server/idp"
|
"github.com/netbirdio/netbird/management/server/idp"
|
||||||
@@ -142,12 +143,22 @@ var (
|
|||||||
if disableSingleAccMode {
|
if disableSingleAccMode {
|
||||||
mgmtSingleAccModeDomain = ""
|
mgmtSingleAccModeDomain = ""
|
||||||
}
|
}
|
||||||
eventStore, err := sqlite.NewSQLiteStore(config.Datadir)
|
eventStore, key, err := initEventStore(config.Datadir, config.DataStoreEncryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to initialize database: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.DataStoreEncryptionKey != key {
|
||||||
|
log.Infof("update config with activity store key")
|
||||||
|
config.DataStoreEncryptionKey = key
|
||||||
|
err := updateMgmtConfig(mgmtConfig, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write out store encryption key: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain,
|
accountManager, err := server.BuildManager(store, peersUpdateManager, idpManager, mgmtSingleAccModeDomain,
|
||||||
dnsDomain, eventStore)
|
dnsDomain, eventStore, userDeleteFromIDPEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to build default manager: %v", err)
|
return fmt.Errorf("failed to build default manager: %v", err)
|
||||||
}
|
}
|
||||||
@@ -287,6 +298,20 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func initEventStore(dataDir string, key string) (activity.Store, string, error) {
|
||||||
|
var err error
|
||||||
|
if key == "" {
|
||||||
|
log.Debugf("generate new activity store encryption key")
|
||||||
|
key, err = sqlite.GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store, err := sqlite.NewSQLiteStore(dataDir, key)
|
||||||
|
return store, key, err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func notifyStop(msg string) {
|
func notifyStop(msg string) {
|
||||||
select {
|
select {
|
||||||
case stopCh <- 1:
|
case stopCh <- 1:
|
||||||
@@ -440,6 +465,10 @@ func loadMgmtConfig(mgmtConfigPath string) (*server.Config, error) {
|
|||||||
return loadedConfig, err
|
return loadedConfig, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateMgmtConfig(path string, config *server.Config) error {
|
||||||
|
return util.DirectWriteJson(path, config)
|
||||||
|
}
|
||||||
|
|
||||||
// OIDCConfigResponse used for parsing OIDC config response
|
// OIDCConfigResponse used for parsing OIDC config response
|
||||||
type OIDCConfigResponse struct {
|
type OIDCConfigResponse struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ var (
|
|||||||
disableMetrics bool
|
disableMetrics bool
|
||||||
disableSingleAccMode bool
|
disableSingleAccMode bool
|
||||||
idpSignKeyRefreshEnabled bool
|
idpSignKeyRefreshEnabled bool
|
||||||
|
userDeleteFromIDPEnabled bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird-mgmt",
|
Use: "netbird-mgmt",
|
||||||
@@ -56,6 +57,7 @@ func init() {
|
|||||||
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")
|
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")
|
||||||
mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max lenght is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain))
|
mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max lenght is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain))
|
||||||
mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, "idp-sign-key-refresh-enabled", false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.")
|
mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, "idp-sign-key-refresh-enabled", false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.")
|
||||||
|
mgmtCmd.Flags().BoolVar(&userDeleteFromIDPEnabled, "user-delete-from-idp", false, "Allows to delete user from IDP when user is deleted from account")
|
||||||
rootCmd.MarkFlagRequired("config") //nolint
|
rootCmd.MarkFlagRequired("config") //nolint
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
|
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "")
|
||||||
|
|||||||
@@ -62,12 +62,9 @@ type AccountManager interface {
|
|||||||
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
|
GetAccountFromPAT(pat string) (*Account, *User, *PersonalAccessToken, error)
|
||||||
MarkPATUsed(tokenID string) error
|
MarkPATUsed(tokenID string) error
|
||||||
GetUser(claims jwtclaims.AuthorizationClaims) (*User, error)
|
GetUser(claims jwtclaims.AuthorizationClaims) (*User, error)
|
||||||
AccountExists(accountId string) (*bool, error)
|
|
||||||
GetPeerByKey(peerKey string) (*Peer, error)
|
|
||||||
GetPeers(accountID, userID string) ([]*Peer, error)
|
GetPeers(accountID, userID string) ([]*Peer, error)
|
||||||
MarkPeerConnected(peerKey string, connected bool) error
|
MarkPeerConnected(peerKey string, connected bool) error
|
||||||
DeletePeer(accountID, peerID, userID string) (*Peer, error)
|
DeletePeer(accountID, peerID, userID string) error
|
||||||
GetPeerByIP(accountId string, peerIP string) (*Peer, error)
|
|
||||||
UpdatePeer(accountID, userID string, peer *Peer) (*Peer, error)
|
UpdatePeer(accountID, userID string, peer *Peer) (*Peer, error)
|
||||||
GetNetworkMap(peerID string) (*NetworkMap, error)
|
GetNetworkMap(peerID string) (*NetworkMap, error)
|
||||||
GetPeerNetwork(peerID string) (*Network, error)
|
GetPeerNetwork(peerID string) (*Network, error)
|
||||||
@@ -80,26 +77,22 @@ type AccountManager interface {
|
|||||||
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
|
GetUsersFromAccount(accountID, userID string) ([]*UserInfo, error)
|
||||||
GetGroup(accountId, groupID string) (*Group, error)
|
GetGroup(accountId, groupID string) (*Group, error)
|
||||||
SaveGroup(accountID, userID string, group *Group) error
|
SaveGroup(accountID, userID string, group *Group) error
|
||||||
UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error)
|
|
||||||
DeleteGroup(accountId, userId, groupID string) error
|
DeleteGroup(accountId, userId, groupID string) error
|
||||||
ListGroups(accountId string) ([]*Group, error)
|
ListGroups(accountId string) ([]*Group, error)
|
||||||
GroupAddPeer(accountId, groupID, peerID string) error
|
GroupAddPeer(accountId, groupID, peerID string) error
|
||||||
GroupDeletePeer(accountId, groupID, peerKey string) error
|
GroupDeletePeer(accountId, groupID, peerID string) error
|
||||||
GroupListPeers(accountId, groupID string) ([]*Peer, error)
|
|
||||||
GetPolicy(accountID, policyID, userID string) (*Policy, error)
|
GetPolicy(accountID, policyID, userID string) (*Policy, error)
|
||||||
SavePolicy(accountID, userID string, policy *Policy) error
|
SavePolicy(accountID, userID string, policy *Policy) error
|
||||||
DeletePolicy(accountID, policyID, userID string) error
|
DeletePolicy(accountID, policyID, userID string) error
|
||||||
ListPolicies(accountID, userID string) ([]*Policy, error)
|
ListPolicies(accountID, userID string) ([]*Policy, error)
|
||||||
GetRoute(accountID, routeID, userID string) (*route.Route, error)
|
GetRoute(accountID, routeID, userID string) (*route.Route, error)
|
||||||
CreateRoute(accountID string, prefix, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error)
|
CreateRoute(accountID, prefix, peerID string, peerGroupIDs []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error)
|
||||||
SaveRoute(accountID, userID string, route *route.Route) error
|
SaveRoute(accountID, userID string, route *route.Route) error
|
||||||
UpdateRoute(accountID, routeID string, operations []RouteUpdateOperation) (*route.Route, error)
|
|
||||||
DeleteRoute(accountID, routeID, userID string) error
|
DeleteRoute(accountID, routeID, userID string) error
|
||||||
ListRoutes(accountID, userID string) ([]*route.Route, error)
|
ListRoutes(accountID, userID string) ([]*route.Route, error)
|
||||||
GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
||||||
CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error)
|
CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error)
|
||||||
SaveNameServerGroup(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
|
SaveNameServerGroup(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
|
||||||
UpdateNameServerGroup(accountID, nsGroupID, userID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
|
|
||||||
DeleteNameServerGroup(accountID, nsGroupID, userID string) error
|
DeleteNameServerGroup(accountID, nsGroupID, userID string) error
|
||||||
ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error)
|
ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error)
|
||||||
GetDNSDomain() string
|
GetDNSDomain() string
|
||||||
@@ -110,6 +103,7 @@ type AccountManager interface {
|
|||||||
UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error)
|
UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error)
|
||||||
LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
||||||
SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) // used by peer gRPC API
|
||||||
|
GetAllConnectedPeers() (map[string]struct{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultAccountManager struct {
|
type DefaultAccountManager struct {
|
||||||
@@ -133,6 +127,9 @@ type DefaultAccountManager struct {
|
|||||||
// dnsDomain is used for peer resolution. This is appended to the peer's name
|
// dnsDomain is used for peer resolution. This is appended to the peer's name
|
||||||
dnsDomain string
|
dnsDomain string
|
||||||
peerLoginExpiry Scheduler
|
peerLoginExpiry Scheduler
|
||||||
|
|
||||||
|
// userDeleteFromIDPEnabled allows to delete user from IDP when user is deleted from account
|
||||||
|
userDeleteFromIDPEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings represents Account settings structure that can be modified via API and Dashboard
|
// Settings represents Account settings structure that can be modified via API and Dashboard
|
||||||
@@ -183,7 +180,7 @@ type Account struct {
|
|||||||
Policies []*Policy
|
Policies []*Policy
|
||||||
Routes map[string]*route.Route
|
Routes map[string]*route.Route
|
||||||
NameServerGroups map[string]*nbdns.NameServerGroup
|
NameServerGroups map[string]*nbdns.NameServerGroup
|
||||||
DNSSettings *DNSSettings
|
DNSSettings DNSSettings
|
||||||
// Settings is a dictionary of Account settings
|
// Settings is a dictionary of Account settings
|
||||||
Settings *Settings
|
Settings *Settings
|
||||||
}
|
}
|
||||||
@@ -204,7 +201,7 @@ type UserInfo struct {
|
|||||||
// from the ACL peers that have distribution groups associated with the peer ID.
|
// from the ACL peers that have distribution groups associated with the peer ID.
|
||||||
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
||||||
func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route {
|
func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route {
|
||||||
routes, peerDisabledRoutes := a.getEnabledAndDisabledRoutesByPeer(peerID)
|
routes, peerDisabledRoutes := a.getRoutingPeerRoutes(peerID)
|
||||||
peerRoutesMembership := make(lookupMap)
|
peerRoutesMembership := make(lookupMap)
|
||||||
for _, r := range append(routes, peerDisabledRoutes...) {
|
for _, r := range append(routes, peerDisabledRoutes...) {
|
||||||
peerRoutesMembership[route.GetHAUniqueID(r)] = struct{}{}
|
peerRoutesMembership[route.GetHAUniqueID(r)] = struct{}{}
|
||||||
@@ -212,7 +209,7 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Rout
|
|||||||
|
|
||||||
groupListMap := a.getPeerGroups(peerID)
|
groupListMap := a.getPeerGroups(peerID)
|
||||||
for _, peer := range aclPeers {
|
for _, peer := range aclPeers {
|
||||||
activeRoutes, _ := a.getEnabledAndDisabledRoutesByPeer(peer.ID)
|
activeRoutes, _ := a.getRoutingPeerRoutes(peer.ID)
|
||||||
groupFilteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap)
|
groupFilteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap)
|
||||||
filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership)
|
filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership)
|
||||||
routes = append(routes, filteredRoutes...)
|
routes = append(routes, filteredRoutes...)
|
||||||
@@ -248,29 +245,63 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku
|
|||||||
return filteredRoutes
|
return filteredRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEnabledAndDisabledRoutesByPeer returns the enabled and disabled lists of routes that belong to a peer.
|
// getRoutingPeerRoutes returns the enabled and disabled lists of routes that the given routing peer serves
|
||||||
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
||||||
func (a *Account) getEnabledAndDisabledRoutesByPeer(peerID string) ([]*route.Route, []*route.Route) {
|
// If the given is not a routing peer, then the lists are empty.
|
||||||
var enabledRoutes []*route.Route
|
func (a *Account) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) {
|
||||||
var disabledRoutes []*route.Route
|
|
||||||
|
peer := a.GetPeer(peerID)
|
||||||
|
if peer == nil {
|
||||||
|
log.Errorf("peer %s that doesn't exist under account %s", peerID, a.Id)
|
||||||
|
return enabledRoutes, disabledRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently we support only linux routing peers
|
||||||
|
if peer.Meta.GoOS != "linux" {
|
||||||
|
return enabledRoutes, disabledRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
seenRoute := make(map[string]struct{})
|
||||||
|
|
||||||
|
takeRoute := func(r *route.Route, id string) {
|
||||||
|
if _, ok := seenRoute[r.ID]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenRoute[r.ID] = struct{}{}
|
||||||
|
|
||||||
|
if r.Enabled {
|
||||||
|
r.Peer = peer.Key
|
||||||
|
enabledRoutes = append(enabledRoutes, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
disabledRoutes = append(disabledRoutes, r)
|
||||||
|
}
|
||||||
|
|
||||||
for _, r := range a.Routes {
|
for _, r := range a.Routes {
|
||||||
|
for _, groupID := range r.PeerGroups {
|
||||||
|
group := a.GetGroup(groupID)
|
||||||
|
if group == nil {
|
||||||
|
log.Errorf("route %s has peers group %s that doesn't exist under account %s", r.ID, groupID, a.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, id := range group.Peers {
|
||||||
|
if id != peerID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeerRoute := r.Copy()
|
||||||
|
newPeerRoute.Peer = id
|
||||||
|
newPeerRoute.PeerGroups = nil
|
||||||
|
newPeerRoute.ID = r.ID + ":" + id // we have to provide unique route id when distribute network map
|
||||||
|
takeRoute(newPeerRoute, id)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
if r.Peer == peerID {
|
if r.Peer == peerID {
|
||||||
// We need to set Peer.Key instead of Peer.ID because this object will be sent to agents as part of a network map.
|
takeRoute(r.Copy(), peerID)
|
||||||
// Ideally we should have a separate field for that, but fine for now.
|
|
||||||
peer := a.GetPeer(peerID)
|
|
||||||
if peer == nil {
|
|
||||||
log.Errorf("route %s has peer %s that doesn't exist under account %s", r.ID, peerID, a.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
raut := r.Copy()
|
|
||||||
raut.Peer = peer.Key
|
|
||||||
if r.Enabled {
|
|
||||||
enabledRoutes = append(enabledRoutes, raut)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
disabledRoutes = append(disabledRoutes, raut)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return enabledRoutes, disabledRoutes
|
return enabledRoutes, disabledRoutes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,17 +317,6 @@ func (a *Account) GetRoutesByPrefix(prefix netip.Prefix) []*route.Route {
|
|||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerByIP returns peer by it's IP if exists under account or nil otherwise
|
|
||||||
func (a *Account) GetPeerByIP(peerIP string) *Peer {
|
|
||||||
for _, peer := range a.Peers {
|
|
||||||
if peerIP == peer.IP.String() {
|
|
||||||
return peer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGroup returns a group by ID if exists, nil otherwise
|
// GetGroup returns a group by ID if exists, nil otherwise
|
||||||
func (a *Account) GetGroup(groupID string) *Group {
|
func (a *Account) GetGroup(groupID string) *Group {
|
||||||
return a.Groups[groupID]
|
return a.Groups[groupID]
|
||||||
@@ -316,7 +336,7 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap {
|
|||||||
}
|
}
|
||||||
peersToConnect = append(peersToConnect, p)
|
peersToConnect = append(peersToConnect, p)
|
||||||
}
|
}
|
||||||
// Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID.
|
|
||||||
routesUpdate := a.getRoutesToSync(peerID, peersToConnect)
|
routesUpdate := a.getRoutesToSync(peerID, peersToConnect)
|
||||||
|
|
||||||
dnsManagementStatus := a.getPeerDNSManagementStatus(peerID)
|
dnsManagementStatus := a.getPeerDNSManagementStatus(peerID)
|
||||||
@@ -493,13 +513,11 @@ func (a *Account) getUserGroups(userID string) ([]string, error) {
|
|||||||
func (a *Account) getPeerDNSManagementStatus(peerID string) bool {
|
func (a *Account) getPeerDNSManagementStatus(peerID string) bool {
|
||||||
peerGroups := a.getPeerGroups(peerID)
|
peerGroups := a.getPeerGroups(peerID)
|
||||||
enabled := true
|
enabled := true
|
||||||
if a.DNSSettings != nil {
|
for _, groupID := range a.DNSSettings.DisabledManagementGroups {
|
||||||
for _, groupID := range a.DNSSettings.DisabledManagementGroups {
|
_, found := peerGroups[groupID]
|
||||||
_, found := peerGroups[groupID]
|
if found {
|
||||||
if found {
|
enabled = false
|
||||||
enabled = false
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return enabled
|
return enabled
|
||||||
@@ -577,8 +595,8 @@ func (a *Account) Copy() *Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
routes := map[string]*route.Route{}
|
routes := map[string]*route.Route{}
|
||||||
for id, route := range a.Routes {
|
for id, r := range a.Routes {
|
||||||
routes[id] = route.Copy()
|
routes[id] = r.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
nsGroups := map[string]*nbdns.NameServerGroup{}
|
nsGroups := map[string]*nbdns.NameServerGroup{}
|
||||||
@@ -586,10 +604,7 @@ func (a *Account) Copy() *Account {
|
|||||||
nsGroups[id] = nsGroup.Copy()
|
nsGroups[id] = nsGroup.Copy()
|
||||||
}
|
}
|
||||||
|
|
||||||
var dnsSettings *DNSSettings
|
dnsSettings := a.DNSSettings.Copy()
|
||||||
if a.DNSSettings != nil {
|
|
||||||
dnsSettings = a.DNSSettings.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings *Settings
|
var settings *Settings
|
||||||
if a.Settings != nil {
|
if a.Settings != nil {
|
||||||
@@ -738,18 +753,19 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) {
|
|||||||
|
|
||||||
// BuildManager creates a new DefaultAccountManager with a provided Store
|
// BuildManager creates a new DefaultAccountManager with a provided Store
|
||||||
func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
|
func BuildManager(store Store, peersUpdateManager *PeersUpdateManager, idpManager idp.Manager,
|
||||||
singleAccountModeDomain string, dnsDomain string, eventStore activity.Store,
|
singleAccountModeDomain string, dnsDomain string, eventStore activity.Store, userDeleteFromIDPEnabled bool,
|
||||||
) (*DefaultAccountManager, error) {
|
) (*DefaultAccountManager, error) {
|
||||||
am := &DefaultAccountManager{
|
am := &DefaultAccountManager{
|
||||||
Store: store,
|
Store: store,
|
||||||
peersUpdateManager: peersUpdateManager,
|
peersUpdateManager: peersUpdateManager,
|
||||||
idpManager: idpManager,
|
idpManager: idpManager,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
cacheMux: sync.Mutex{},
|
cacheMux: sync.Mutex{},
|
||||||
cacheLoading: map[string]chan struct{}{},
|
cacheLoading: map[string]chan struct{}{},
|
||||||
dnsDomain: dnsDomain,
|
dnsDomain: dnsDomain,
|
||||||
eventStore: eventStore,
|
eventStore: eventStore,
|
||||||
peerLoginExpiry: NewDefaultScheduler(),
|
peerLoginExpiry: NewDefaultScheduler(),
|
||||||
|
userDeleteFromIDPEnabled: userDeleteFromIDPEnabled,
|
||||||
}
|
}
|
||||||
allAccounts := store.GetAllAccounts()
|
allAccounts := store.GetAllAccounts()
|
||||||
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
|
// enable single account mode only if configured by user and number of existing accounts is not grater than 1
|
||||||
@@ -874,33 +890,19 @@ func (am *DefaultAccountManager) peerLoginExpirationJob(accountID string) func()
|
|||||||
return account.GetNextPeerExpiration()
|
return account.GetNextPeerExpiration()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expiredPeers := account.GetExpiredPeers()
|
||||||
var peerIDs []string
|
var peerIDs []string
|
||||||
for _, peer := range account.GetExpiredPeers() {
|
for _, peer := range expiredPeers {
|
||||||
if peer.Status.LoginExpired {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
peerIDs = append(peerIDs, peer.ID)
|
peerIDs = append(peerIDs, peer.ID)
|
||||||
peer.MarkLoginExpired(true)
|
|
||||||
account.UpdatePeer(peer)
|
|
||||||
err = am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
|
||||||
return account.GetNextPeerExpiration()
|
|
||||||
}
|
|
||||||
am.storeEvent(peer.UserID, peer.ID, account.Id, activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
|
log.Debugf("discovered %d peers to expire for account %s", len(peerIDs), account.Id)
|
||||||
|
|
||||||
if len(peerIDs) != 0 {
|
if err := am.expireAndUpdatePeers(account, expiredPeers); err != nil {
|
||||||
// this will trigger peer disconnect from the management service
|
log.Errorf("failed updating account peers while expiring peers for account %s", account.Id)
|
||||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
return account.GetNextPeerExpiration()
|
||||||
err = am.updateAccountPeers(account)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed updating account peers while expiring peers for account %s", accountID)
|
|
||||||
return account.GetNextPeerExpiration()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return account.GetNextPeerExpiration()
|
return account.GetNextPeerExpiration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -940,6 +942,28 @@ func (am *DefaultAccountManager) warmupIDPCache() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log.Infof("%d entries received from IdP management", len(userData))
|
||||||
|
|
||||||
|
// If the Identity Provider does not support writing AppMetadata,
|
||||||
|
// in cases like this, we expect it to return all users in an "unset" field.
|
||||||
|
// We iterate over the users in the "unset" field, look up their AccountID in our store, and
|
||||||
|
// update their AppMetadata with the AccountID.
|
||||||
|
if unsetData, ok := userData[idp.UnsetAccountID]; ok {
|
||||||
|
for _, user := range unsetData {
|
||||||
|
accountID, err := am.Store.GetAccountByUser(user.ID)
|
||||||
|
if err == nil {
|
||||||
|
data := userData[accountID.Id]
|
||||||
|
if data == nil {
|
||||||
|
data = make([]*idp.UserData, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.AppMetadata.WTAccountID = accountID.Id
|
||||||
|
|
||||||
|
userData[accountID.Id] = append(data, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(userData, idp.UnsetAccountID)
|
||||||
|
|
||||||
for accountID, users := range userData {
|
for accountID, users := range userData {
|
||||||
err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration()))
|
err = am.cacheManager.Set(am.ctx, accountID, users, cacheStore.WithExpiration(cacheEntryExpiration()))
|
||||||
@@ -1007,7 +1031,37 @@ func (am *DefaultAccountManager) addAccountIDToIDPAppMeta(userID string, account
|
|||||||
|
|
||||||
func (am *DefaultAccountManager) loadAccount(_ context.Context, accountID interface{}) ([]*idp.UserData, error) {
|
func (am *DefaultAccountManager) loadAccount(_ context.Context, accountID interface{}) ([]*idp.UserData, error) {
|
||||||
log.Debugf("account %s not found in cache, reloading", accountID)
|
log.Debugf("account %s not found in cache, reloading", accountID)
|
||||||
return am.idpManager.GetAccount(fmt.Sprintf("%v", accountID))
|
accountIDString := fmt.Sprintf("%v", accountID)
|
||||||
|
|
||||||
|
account, err := am.Store.GetAccount(accountIDString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userData, err := am.idpManager.GetAccount(accountIDString)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("%d entries received from IdP management", len(userData))
|
||||||
|
|
||||||
|
dataMap := make(map[string]*idp.UserData, len(userData))
|
||||||
|
for _, datum := range userData {
|
||||||
|
dataMap[datum.ID] = datum
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedUserData := make([]*idp.UserData, 0)
|
||||||
|
for _, user := range account.Users {
|
||||||
|
if user.IsServiceUser {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
datum, ok := dataMap[user.Id]
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("user %s not found in IDP", user.Id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matchedUserData = append(matchedUserData, datum)
|
||||||
|
}
|
||||||
|
return matchedUserData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *DefaultAccountManager) lookupUserInCacheByEmail(email string, accountID string) (*idp.UserData, error) {
|
func (am *DefaultAccountManager) lookupUserInCacheByEmail(email string, accountID string) (*idp.UserData, error) {
|
||||||
@@ -1256,7 +1310,6 @@ func (am *DefaultAccountManager) redeemInvite(account *Account, userID string) e
|
|||||||
|
|
||||||
// MarkPATUsed marks a personal access token as used
|
// MarkPATUsed marks a personal access token as used
|
||||||
func (am *DefaultAccountManager) MarkPATUsed(tokenID string) error {
|
func (am *DefaultAccountManager) MarkPATUsed(tokenID string) error {
|
||||||
unlock := am.Store.AcquireGlobalLock()
|
|
||||||
|
|
||||||
user, err := am.Store.GetUserByTokenID(tokenID)
|
user, err := am.Store.GetUserByTokenID(tokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1268,8 +1321,7 @@ func (am *DefaultAccountManager) MarkPATUsed(tokenID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
unlock()
|
unlock := am.Store.AcquireAccountLock(account.Id)
|
||||||
unlock = am.Store.AcquireAccountLock(account.Id)
|
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
account, err = am.Store.GetAccountByUser(user.Id)
|
account, err = am.Store.GetAccountByUser(user.Id)
|
||||||
@@ -1396,9 +1448,7 @@ func (am *DefaultAccountManager) GetAccountFromToken(claims jwtclaims.Authorizat
|
|||||||
if err := am.Store.SaveAccount(account); err != nil {
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
log.Errorf("failed to save account: %v", err)
|
log.Errorf("failed to save account: %v", err)
|
||||||
} else {
|
} else {
|
||||||
if err := am.updateAccountPeers(account); err != nil {
|
am.updateAccountPeers(account)
|
||||||
log.Errorf("failed updating account peers while updating user %s", account.Id)
|
|
||||||
}
|
|
||||||
for _, g := range addNewGroups {
|
for _, g := range addNewGroups {
|
||||||
if group := account.GetGroup(g); group != nil {
|
if group := account.GetGroup(g); group != nil {
|
||||||
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser,
|
am.storeEvent(user.Id, user.Id, account.Id, activity.GroupAddedToUser,
|
||||||
@@ -1504,31 +1554,16 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(claims jwtcla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllConnectedPeers returns connected peers based on peersUpdateManager.GetAllConnectedPeers()
|
||||||
|
func (am *DefaultAccountManager) GetAllConnectedPeers() (map[string]struct{}, error) {
|
||||||
|
return am.peersUpdateManager.GetAllConnectedPeers(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func isDomainValid(domain string) bool {
|
func isDomainValid(domain string) bool {
|
||||||
re := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
re := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
|
||||||
return re.Match([]byte(domain))
|
return re.Match([]byte(domain))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountExists checks whether account exists (returns true) or not (returns false)
|
|
||||||
func (am *DefaultAccountManager) AccountExists(accountID string) (*bool, error) {
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
var res bool
|
|
||||||
_, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
if s, ok := status.FromError(err); ok && s.Type() == status.NotFound {
|
|
||||||
res = false
|
|
||||||
return &res, nil
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res = true
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDNSDomain returns the configured dnsDomain
|
// GetDNSDomain returns the configured dnsDomain
|
||||||
func (am *DefaultAccountManager) GetDNSDomain() string {
|
func (am *DefaultAccountManager) GetDNSDomain() string {
|
||||||
return am.dnsDomain
|
return am.dnsDomain
|
||||||
@@ -1578,7 +1613,7 @@ func newAccountWithId(accountID, userID, domain string) *Account {
|
|||||||
setupKeys := map[string]*SetupKey{}
|
setupKeys := map[string]*SetupKey{}
|
||||||
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
nameServersGroups := make(map[string]*nbdns.NameServerGroup)
|
||||||
users[userID] = NewAdminUser(userID)
|
users[userID] = NewAdminUser(userID)
|
||||||
dnsSettings := &DNSSettings{
|
dnsSettings := DNSSettings{
|
||||||
DisabledManagementGroups: make([]string, 0),
|
DisabledManagementGroups: make([]string, 0),
|
||||||
}
|
}
|
||||||
log.Debugf("created new account %s", accountID)
|
log.Debugf("created new account %s", accountID)
|
||||||
@@ -1605,19 +1640,3 @@ func newAccountWithId(accountID, userID, domain string) *Account {
|
|||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFromList(inputList []string, toRemove []string) []string {
|
|
||||||
toRemoveMap := make(map[string]struct{})
|
|
||||||
for _, item := range toRemove {
|
|
||||||
toRemoveMap[item] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var resultList []string
|
|
||||||
for _, item := range inputList {
|
|
||||||
_, ok := toRemoveMap[item]
|
|
||||||
if !ok {
|
|
||||||
resultList = append(resultList, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resultList
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -706,30 +706,6 @@ func createAccount(am *DefaultAccountManager, accountID, userID, domain string)
|
|||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccountManager_AccountExists(t *testing.T) {
|
|
||||||
manager, err := createManager(t)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedId := "test_account"
|
|
||||||
userId := "account_creator"
|
|
||||||
_, err = createAccount(manager, expectedId, userId, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err := manager.AccountExists(expectedId)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*exists {
|
|
||||||
t.Errorf("expected account to exist after creation, got false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccountManager_GetAccount(t *testing.T) {
|
func TestAccountManager_GetAccount(t *testing.T) {
|
||||||
manager, err := createManager(t)
|
manager, err := createManager(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1062,7 +1038,7 @@ func TestAccountManager_NetworkUpdates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if _, err := manager.DeletePeer(account.Id, peer3.ID, userID); err != nil {
|
if err := manager.DeletePeer(account.Id, peer3.ID, userID); err != nil {
|
||||||
t.Errorf("delete peer: %v", err)
|
t.Errorf("delete peer: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1129,7 +1105,7 @@ func TestAccountManager_DeletePeer(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = manager.DeletePeer(account.Id, peerKey, userID)
|
err = manager.DeletePeer(account.Id, peerKey, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1237,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
account := &Account{
|
account := &Account{
|
||||||
Peers: map[string]*Peer{
|
Peers: map[string]*Peer{
|
||||||
"peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"},
|
"peer-1": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-2": {Key: "peer-2", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-3": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}},
|
||||||
},
|
},
|
||||||
Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}},
|
Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}},
|
||||||
Routes: map[string]*route.Route{
|
Routes: map[string]*route.Route{
|
||||||
@@ -1385,8 +1361,9 @@ func TestAccount_Copy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Routes: map[string]*route.Route{
|
Routes: map[string]*route.Route{
|
||||||
"route1": {
|
"route1": {
|
||||||
ID: "route1",
|
ID: "route1",
|
||||||
Groups: []string{"group1"},
|
PeerGroups: []string{},
|
||||||
|
Groups: []string{"group1"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||||
@@ -1397,7 +1374,7 @@ func TestAccount_Copy(t *testing.T) {
|
|||||||
NameServers: []nbdns.NameServer{},
|
NameServers: []nbdns.NameServer{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}},
|
DNSSettings: DNSSettings{DisabledManagementGroups: []string{}},
|
||||||
Settings: &Settings{},
|
Settings: &Settings{},
|
||||||
}
|
}
|
||||||
err := hasNilField(account)
|
err := hasNilField(account)
|
||||||
@@ -2063,7 +2040,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore)
|
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.cloud", eventStore, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStore(t *testing.T) (Store, error) {
|
func createStore(t *testing.T) (Store, error) {
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ const (
|
|||||||
UserBlocked
|
UserBlocked
|
||||||
// UserUnblocked indicates that a user unblocked another user
|
// UserUnblocked indicates that a user unblocked another user
|
||||||
UserUnblocked
|
UserUnblocked
|
||||||
|
// UserDeleted indicates that a user deleted another user
|
||||||
|
UserDeleted
|
||||||
// GroupDeleted indicates that a user deleted group
|
// GroupDeleted indicates that a user deleted group
|
||||||
GroupDeleted
|
GroupDeleted
|
||||||
// UserLoggedInPeer indicates that user logged in their peer with an interactive SSO login
|
// UserLoggedInPeer indicates that user logged in their peer with an interactive SSO login
|
||||||
@@ -162,6 +164,7 @@ var activityMap = map[Activity]Code{
|
|||||||
ServiceUserDeleted: {"Service user deleted", "service.user.delete"},
|
ServiceUserDeleted: {"Service user deleted", "service.user.delete"},
|
||||||
UserBlocked: {"User blocked", "user.block"},
|
UserBlocked: {"User blocked", "user.block"},
|
||||||
UserUnblocked: {"User unblocked", "user.unblock"},
|
UserUnblocked: {"User unblocked", "user.unblock"},
|
||||||
|
UserDeleted: {"User deleted", "user.delete"},
|
||||||
GroupDeleted: {"Group deleted", "group.delete"},
|
GroupDeleted: {"Group deleted", "group.delete"},
|
||||||
UserLoggedInPeer: {"User logged in peer", "user.peer.login"},
|
UserLoggedInPeer: {"User logged in peer", "user.peer.login"},
|
||||||
PeerLoginExpired: {"Peer login expired", "peer.login.expire"},
|
PeerLoginExpired: {"Peer login expired", "peer.login.expire"},
|
||||||
|
|||||||
@@ -18,10 +18,15 @@ type Event struct {
|
|||||||
ID uint64
|
ID uint64
|
||||||
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
|
// InitiatorID is the ID of an object that initiated the event (e.g., a user)
|
||||||
InitiatorID string
|
InitiatorID string
|
||||||
|
// InitiatorName is the name of an object that initiated the event.
|
||||||
|
InitiatorName string
|
||||||
|
// InitiatorEmail is the email address of an object that initiated the event.
|
||||||
|
InitiatorEmail string
|
||||||
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
|
// TargetID is the ID of an object that was effected by the event (e.g., a peer)
|
||||||
TargetID string
|
TargetID string
|
||||||
// AccountID is the ID of an account where the event happened
|
// AccountID is the ID of an account where the event happened
|
||||||
AccountID string
|
AccountID string
|
||||||
|
|
||||||
// Meta of the event, e.g. deleted peer information like name, IP, etc
|
// Meta of the event, e.g. deleted peer information like name, IP, etc
|
||||||
Meta map[string]any
|
Meta map[string]any
|
||||||
}
|
}
|
||||||
@@ -35,12 +40,14 @@ func (e *Event) Copy() *Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Event{
|
return &Event{
|
||||||
Timestamp: e.Timestamp,
|
Timestamp: e.Timestamp,
|
||||||
Activity: e.Activity,
|
Activity: e.Activity,
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
InitiatorID: e.InitiatorID,
|
InitiatorID: e.InitiatorID,
|
||||||
TargetID: e.TargetID,
|
InitiatorName: e.InitiatorName,
|
||||||
AccountID: e.AccountID,
|
InitiatorEmail: e.InitiatorEmail,
|
||||||
Meta: meta,
|
TargetID: e.TargetID,
|
||||||
|
AccountID: e.AccountID,
|
||||||
|
Meta: meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
management/server/activity/sqlite/crypt.go
Normal file
81
management/server/activity/sqlite/crypt.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var iv = []byte{10, 22, 13, 79, 05, 8, 52, 91, 87, 98, 88, 98, 35, 25, 13, 05}
|
||||||
|
|
||||||
|
type FieldEncrypt struct {
|
||||||
|
block cipher.Block
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateKey() (string, error) {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
readableKey := base64.StdEncoding.EncodeToString(key)
|
||||||
|
return readableKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFieldEncrypt(key string) (*FieldEncrypt, error) {
|
||||||
|
binKey, err := base64.StdEncoding.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(binKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ec := &FieldEncrypt{
|
||||||
|
block: block,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *FieldEncrypt) Encrypt(payload string) string {
|
||||||
|
plainText := pkcs5Padding([]byte(payload))
|
||||||
|
cipherText := make([]byte, len(plainText))
|
||||||
|
cbc := cipher.NewCBCEncrypter(ec.block, iv)
|
||||||
|
cbc.CryptBlocks(cipherText, plainText)
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *FieldEncrypt) Decrypt(data string) (string, error) {
|
||||||
|
cipherText, err := base64.StdEncoding.DecodeString(data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cbc := cipher.NewCBCDecrypter(ec.block, iv)
|
||||||
|
cbc.CryptBlocks(cipherText, cipherText)
|
||||||
|
payload, err := pkcs5UnPadding(cipherText)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(payload), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs5Padding(ciphertext []byte) []byte {
|
||||||
|
padding := aes.BlockSize - len(ciphertext)%aes.BlockSize
|
||||||
|
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padText...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs5UnPadding(src []byte) ([]byte, error) {
|
||||||
|
srcLen := len(src)
|
||||||
|
paddingLen := int(src[srcLen-1])
|
||||||
|
if paddingLen >= srcLen || paddingLen > aes.BlockSize {
|
||||||
|
return nil, fmt.Errorf("padding size error")
|
||||||
|
}
|
||||||
|
return src[:srcLen-paddingLen], nil
|
||||||
|
}
|
||||||
63
management/server/activity/sqlite/crypt_test.go
Normal file
63
management/server/activity/sqlite/crypt_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateKey(t *testing.T) {
|
||||||
|
testData := "exampl@netbird.io"
|
||||||
|
key, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %s", err)
|
||||||
|
}
|
||||||
|
ee, err := NewFieldEncrypt(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init email encryption: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted := ee.Encrypt(testData)
|
||||||
|
if encrypted == "" {
|
||||||
|
t.Fatalf("invalid encrypted text")
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := ee.Decrypt(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decrypt data: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decrypted != testData {
|
||||||
|
t.Fatalf("decrypted data is not match with test data: %s, %s", testData, decrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorruptKey(t *testing.T) {
|
||||||
|
testData := "exampl@netbird.io"
|
||||||
|
key, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %s", err)
|
||||||
|
}
|
||||||
|
ee, err := NewFieldEncrypt(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init email encryption: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted := ee.Encrypt(testData)
|
||||||
|
if encrypted == "" {
|
||||||
|
t.Fatalf("invalid encrypted text")
|
||||||
|
}
|
||||||
|
|
||||||
|
newKey, err := GenerateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ee, err = NewFieldEncrypt(newKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to init email encryption: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, _ := ee.Decrypt(encrypted)
|
||||||
|
if res == testData {
|
||||||
|
t.Fatalf("incorrect decryption, the result is: %s", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@ package sqlite
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
|
||||||
|
|
||||||
// sqlite driver
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,69 +25,126 @@ const (
|
|||||||
"meta TEXT," +
|
"meta TEXT," +
|
||||||
" target_id TEXT);"
|
" target_id TEXT);"
|
||||||
|
|
||||||
selectDescQuery = "SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta" +
|
creatTableDeletedUsersQuery = `CREATE TABLE IF NOT EXISTS deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`
|
||||||
" FROM events WHERE account_id = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?;"
|
|
||||||
selectAscQuery = "SELECT id, activity, timestamp, initiator_id, target_id, account_id, meta" +
|
selectDescQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
|
||||||
" FROM events WHERE account_id = ? ORDER BY timestamp ASC LIMIT ? OFFSET ?;"
|
FROM events
|
||||||
|
LEFT JOIN deleted_users i ON events.initiator_id = i.id
|
||||||
|
LEFT JOIN deleted_users t ON events.target_id = t.id
|
||||||
|
WHERE account_id = ?
|
||||||
|
ORDER BY timestamp DESC LIMIT ? OFFSET ?;`
|
||||||
|
|
||||||
|
selectAscQuery = `SELECT events.id, activity, timestamp, initiator_id, i.name as "initiator_name", i.email as "initiator_email", target_id, t.name as "target_name", t.email as "target_email", account_id, meta
|
||||||
|
FROM events
|
||||||
|
LEFT JOIN deleted_users i ON events.initiator_id = i.id
|
||||||
|
LEFT JOIN deleted_users t ON events.target_id = t.id
|
||||||
|
WHERE account_id = ?
|
||||||
|
ORDER BY timestamp ASC LIMIT ? OFFSET ?;`
|
||||||
|
|
||||||
insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
|
insertQuery = "INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) " +
|
||||||
"VALUES(?, ?, ?, ?, ?, ?)"
|
"VALUES(?, ?, ?, ?, ?, ?)"
|
||||||
|
|
||||||
|
insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`
|
||||||
|
|
||||||
|
fallbackName = "unknown"
|
||||||
|
fallbackEmail = "unknown@unknown.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store is the implementation of the activity.Store interface backed by SQLite
|
// Store is the implementation of the activity.Store interface backed by SQLite
|
||||||
type Store struct {
|
type Store struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
fieldEncrypt *FieldEncrypt
|
||||||
|
|
||||||
insertStatement *sql.Stmt
|
insertStatement *sql.Stmt
|
||||||
selectAscStatement *sql.Stmt
|
selectAscStatement *sql.Stmt
|
||||||
selectDescStatement *sql.Stmt
|
selectDescStatement *sql.Stmt
|
||||||
|
deleteUserStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSQLiteStore creates a new Store with an event table if not exists.
|
// NewSQLiteStore creates a new Store with an event table if not exists.
|
||||||
func NewSQLiteStore(dataDir string) (*Store, error) {
|
func NewSQLiteStore(dataDir string, encryptionKey string) (*Store, error) {
|
||||||
dbFile := filepath.Join(dataDir, eventSinkDB)
|
dbFile := filepath.Join(dataDir, eventSinkDB)
|
||||||
db, err := sql.Open("sqlite3", dbFile)
|
db, err := sql.Open("sqlite3", dbFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
crypt, err := NewFieldEncrypt(encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
_, err = db.Exec(createTableQuery)
|
_, err = db.Exec(createTableQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(creatTableDeletedUsersQuery)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateDeletedUsersTable(db)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
insertStmt, err := db.Prepare(insertQuery)
|
insertStmt, err := db.Prepare(insertQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
selectDescStmt, err := db.Prepare(selectDescQuery)
|
selectDescStmt, err := db.Prepare(selectDescQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAscStmt, err := db.Prepare(selectAscQuery)
|
selectAscStmt, err := db.Prepare(selectAscQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Store{
|
deleteUserStmt, err := db.Prepare(insertDeleteUserQuery)
|
||||||
|
if err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &Store{
|
||||||
db: db,
|
db: db,
|
||||||
|
fieldEncrypt: crypt,
|
||||||
insertStatement: insertStmt,
|
insertStatement: insertStmt,
|
||||||
selectDescStatement: selectDescStmt,
|
selectDescStatement: selectDescStmt,
|
||||||
selectAscStatement: selectAscStmt,
|
selectAscStatement: selectAscStmt,
|
||||||
}, nil
|
deleteUserStmt: deleteUserStmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) {
|
||||||
events := make([]*activity.Event, 0)
|
events := make([]*activity.Event, 0)
|
||||||
|
var cryptErr error
|
||||||
for result.Next() {
|
for result.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
var operation activity.Activity
|
var operation activity.Activity
|
||||||
var timestamp time.Time
|
var timestamp time.Time
|
||||||
var initiator string
|
var initiator string
|
||||||
|
var initiatorName *string
|
||||||
|
var initiatorEmail *string
|
||||||
var target string
|
var target string
|
||||||
|
var targetUserName *string
|
||||||
|
var targetEmail *string
|
||||||
var account string
|
var account string
|
||||||
var jsonMeta string
|
var jsonMeta string
|
||||||
err := result.Scan(&id, &operation, ×tamp, &initiator, &target, &account, &jsonMeta)
|
err := result.Scan(&id, &operation, ×tamp, &initiator, &initiatorName, &initiatorEmail, &target, &targetUserName, &targetEmail, &account, &jsonMeta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,7 +157,27 @@ func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events = append(events, &activity.Event{
|
if targetUserName != nil {
|
||||||
|
name, err := store.fieldEncrypt.Decrypt(*targetUserName)
|
||||||
|
if err != nil {
|
||||||
|
cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target)
|
||||||
|
meta["username"] = fallbackName
|
||||||
|
} else {
|
||||||
|
meta["username"] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetEmail != nil {
|
||||||
|
email, err := store.fieldEncrypt.Decrypt(*targetEmail)
|
||||||
|
if err != nil {
|
||||||
|
cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target)
|
||||||
|
meta["email"] = fallbackEmail
|
||||||
|
} else {
|
||||||
|
meta["email"] = email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event := &activity.Event{
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
Activity: operation,
|
Activity: operation,
|
||||||
ID: uint64(id),
|
ID: uint64(id),
|
||||||
@@ -108,7 +185,33 @@ func processResult(result *sql.Rows) ([]*activity.Event, error) {
|
|||||||
TargetID: target,
|
TargetID: target,
|
||||||
AccountID: account,
|
AccountID: account,
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if initiatorName != nil {
|
||||||
|
name, err := store.fieldEncrypt.Decrypt(*initiatorName)
|
||||||
|
if err != nil {
|
||||||
|
cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator)
|
||||||
|
event.InitiatorName = fallbackName
|
||||||
|
} else {
|
||||||
|
event.InitiatorName = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if initiatorEmail != nil {
|
||||||
|
email, err := store.fieldEncrypt.Decrypt(*initiatorEmail)
|
||||||
|
if err != nil {
|
||||||
|
cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator)
|
||||||
|
event.InitiatorEmail = fallbackEmail
|
||||||
|
} else {
|
||||||
|
event.InitiatorEmail = email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events = append(events, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cryptErr != nil {
|
||||||
|
log.Warnf("%s", cryptErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return events, nil
|
return events, nil
|
||||||
@@ -127,13 +230,18 @@ func (store *Store) Get(accountID string, offset, limit int, descending bool) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer result.Close() //nolint
|
defer result.Close() //nolint
|
||||||
return processResult(result)
|
return store.processResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save an event in the SQLite events table
|
// Save an event in the SQLite events table end encrypt the "email" element in meta map
|
||||||
func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
|
func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
|
||||||
var jsonMeta string
|
var jsonMeta string
|
||||||
if event.Meta != nil {
|
meta, err := store.saveDeletedUserEmailAndNameInEncrypted(event)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta != nil {
|
||||||
metaBytes, err := json.Marshal(event.Meta)
|
metaBytes, err := json.Marshal(event.Meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -156,6 +264,34 @@ func (store *Store) Save(event *activity.Event) (*activity.Event, error) {
|
|||||||
return eventCopy, nil
|
return eventCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveDeletedUserEmailAndNameInEncrypted if the meta contains email and name then store it in encrypted way and delete
|
||||||
|
// this item from meta map
|
||||||
|
func (store *Store) saveDeletedUserEmailAndNameInEncrypted(event *activity.Event) (map[string]any, error) {
|
||||||
|
email, ok := event.Meta["email"]
|
||||||
|
if !ok {
|
||||||
|
return event.Meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := event.Meta["name"]
|
||||||
|
if !ok {
|
||||||
|
return event.Meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedEmail := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", email))
|
||||||
|
encryptedName := store.fieldEncrypt.Encrypt(fmt.Sprintf("%s", name))
|
||||||
|
_, err := store.deleteUserStmt.Exec(event.TargetID, encryptedEmail, encryptedName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Meta) == 2 {
|
||||||
|
return nil, nil // nolint
|
||||||
|
}
|
||||||
|
delete(event.Meta, "email")
|
||||||
|
delete(event.Meta, "name")
|
||||||
|
return event.Meta, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Close the Store
|
// Close the Store
|
||||||
func (store *Store) Close() error {
|
func (store *Store) Close() error {
|
||||||
if store.db != nil {
|
if store.db != nil {
|
||||||
@@ -163,3 +299,44 @@ func (store *Store) Close() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateDeletedUsersTable(db *sql.DB) error {
|
||||||
|
log.Debugf("check deleted_users table version")
|
||||||
|
rows, err := db.Query(`PRAGMA table_info(deleted_users);`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
found := false
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
cid int
|
||||||
|
name string
|
||||||
|
dataType string
|
||||||
|
notNull int
|
||||||
|
dfltVal sql.NullString
|
||||||
|
pk int
|
||||||
|
)
|
||||||
|
err := rows.Scan(&cid, &name, &dataType, ¬Null, &dfltVal, &pk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if name == "name" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("update delted_users table")
|
||||||
|
_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN name TEXT;`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
|
|
||||||
func TestNewSQLiteStore(t *testing.T) {
|
func TestNewSQLiteStore(t *testing.T) {
|
||||||
dataDir := t.TempDir()
|
dataDir := t.TempDir()
|
||||||
store, err := NewSQLiteStore(dataDir)
|
key, _ := GenerateKey()
|
||||||
|
store, err := NewSQLiteStore(dataDir, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ type Config struct {
|
|||||||
TURNConfig *TURNConfig
|
TURNConfig *TURNConfig
|
||||||
Signal *Host
|
Signal *Host
|
||||||
|
|
||||||
Datadir string
|
Datadir string
|
||||||
|
DataStoreEncryptionKey string
|
||||||
|
|
||||||
HttpConfig *HttpServerConfig
|
HttpConfig *HttpServerConfig
|
||||||
|
|
||||||
|
|||||||
@@ -24,19 +24,11 @@ type DNSSettings struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy returns a copy of the DNS settings
|
// Copy returns a copy of the DNS settings
|
||||||
func (d *DNSSettings) Copy() *DNSSettings {
|
func (d DNSSettings) Copy() DNSSettings {
|
||||||
settings := &DNSSettings{
|
settings := DNSSettings{
|
||||||
DisabledManagementGroups: make([]string, 0),
|
DisabledManagementGroups: make([]string, len(d.DisabledManagementGroups)),
|
||||||
}
|
}
|
||||||
|
copy(settings.DisabledManagementGroups, d.DisabledManagementGroups)
|
||||||
if d == nil {
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.DisabledManagementGroups != nil && len(d.DisabledManagementGroups) > 0 {
|
|
||||||
settings.DisabledManagementGroups = d.DisabledManagementGroups[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +50,8 @@ func (am *DefaultAccountManager) GetDNSSettings(accountID string, userID string)
|
|||||||
if !user.IsAdmin() {
|
if !user.IsAdmin() {
|
||||||
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view DNS settings")
|
return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view DNS settings")
|
||||||
}
|
}
|
||||||
|
dnsSettings := account.DNSSettings.Copy()
|
||||||
if account.DNSSettings == nil {
|
return &dnsSettings, nil
|
||||||
return &DNSSettings{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.DNSSettings.Copy(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveDNSSettings validates a user role and updates the account's DNS settings
|
// SaveDNSSettings validates a user role and updates the account's DNS settings
|
||||||
@@ -96,11 +84,7 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
oldSettings := &DNSSettings{}
|
oldSettings := account.DNSSettings.Copy()
|
||||||
if account.DNSSettings != nil {
|
|
||||||
oldSettings = account.DNSSettings.Copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
account.DNSSettings = dnsSettingsToSave.Copy()
|
account.DNSSettings = dnsSettingsToSave.Copy()
|
||||||
|
|
||||||
account.Network.IncSerial()
|
account.Network.IncSerial()
|
||||||
@@ -122,7 +106,9 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string
|
|||||||
am.storeEvent(userID, accountID, accountID, activity.GroupRemovedFromDisabledManagementGroups, meta)
|
am.storeEvent(userID, accountID, accountID, activity.GroupRemovedFromDisabledManagementGroups, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig {
|
func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) {
|
|||||||
t.Fatal("DNS settings for new accounts shouldn't return nil")
|
t.Fatal("DNS settings for new accounts shouldn't return nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
account.DNSSettings = &DNSSettings{
|
account.DNSSettings = DNSSettings{
|
||||||
DisabledManagementGroups: []string{group1ID},
|
DisabledManagementGroups: []string{group1ID},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.test", eventStore)
|
return BuildManager(store, NewPeersUpdateManager(), nil, "", "netbird.test", eventStore, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDNSStore(t *testing.T) (Store, error) {
|
func createDNSStore(t *testing.T) (Store, error) {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func (e *EphemeralManager) cleanup() {
|
|||||||
|
|
||||||
for id, p := range deletePeers {
|
for id, p := range deletePeers {
|
||||||
log.Debugf("delete ephemeral peer: %s", id)
|
log.Debugf("delete ephemeral peer: %s", id)
|
||||||
_, err := e.accountManager.DeletePeer(p.account.Id, id, activity.SystemInitiator)
|
err := e.accountManager.DeletePeer(p.account.Id, id, activity.SystemInitiator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Tracef("failed to delete ephemeral peer: %s", err)
|
log.Tracef("failed to delete ephemeral peer: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ type MocAccountManager struct {
|
|||||||
store *MockStore
|
store *MockStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a MocAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) {
|
func (a MocAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||||
delete(a.store.account.Peers, peerID)
|
delete(a.store.account.Peers, peerID)
|
||||||
return nil, nil //nolint:nilnil
|
return nil //nolint:nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewManager(t *testing.T) {
|
func TestNewManager(t *testing.T) {
|
||||||
|
|||||||
@@ -111,10 +111,6 @@ func restore(file string) (*FileStore, error) {
|
|||||||
for _, peer := range account.Peers {
|
for _, peer := range account.Peers {
|
||||||
store.PeerKeyID2AccountID[peer.Key] = accountID
|
store.PeerKeyID2AccountID[peer.Key] = accountID
|
||||||
store.PeerID2AccountID[peer.ID] = accountID
|
store.PeerID2AccountID[peer.ID] = accountID
|
||||||
// reset all peers to status = Disconnected
|
|
||||||
if peer.Status != nil && peer.Status.Connected {
|
|
||||||
peer.Status.Connected = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, user := range account.Users {
|
for _, user := range account.Users {
|
||||||
store.UserID2AccountID[user.Id] = accountID
|
store.UserID2AccountID[user.Id] = accountID
|
||||||
|
|||||||
@@ -33,26 +33,6 @@ type Group struct {
|
|||||||
Peers []string
|
Peers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
// UpdateGroupName indicates a name update operation
|
|
||||||
UpdateGroupName GroupUpdateOperationType = iota
|
|
||||||
// InsertPeersToGroup indicates insert peers to group operation
|
|
||||||
InsertPeersToGroup
|
|
||||||
// RemovePeersFromGroup indicates a remove peers from group operation
|
|
||||||
RemovePeersFromGroup
|
|
||||||
// UpdateGroupPeers indicates a replacement of group peers list
|
|
||||||
UpdateGroupPeers
|
|
||||||
)
|
|
||||||
|
|
||||||
// GroupUpdateOperationType operation type
|
|
||||||
type GroupUpdateOperationType int
|
|
||||||
|
|
||||||
// GroupUpdateOperation operation object with type and values to be applied
|
|
||||||
type GroupUpdateOperation struct {
|
|
||||||
Type GroupUpdateOperationType
|
|
||||||
Values []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventMeta returns activity event meta related to the group
|
// EventMeta returns activity event meta related to the group
|
||||||
func (g *Group) EventMeta() map[string]any {
|
func (g *Group) EventMeta() map[string]any {
|
||||||
return map[string]any{"name": g.Name}
|
return map[string]any{"name": g.Name}
|
||||||
@@ -104,10 +84,7 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// the following snippet tracks the activity and stores the group events in the event store.
|
// the following snippet tracks the activity and stores the group events in the event store.
|
||||||
// It has to happen after all the operations have been successfully performed.
|
// It has to happen after all the operations have been successfully performed.
|
||||||
@@ -165,57 +142,6 @@ func difference(a, b []string) []string {
|
|||||||
return diff
|
return diff
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGroup updates a group using a list of operations
|
|
||||||
func (am *DefaultAccountManager) UpdateGroup(accountID string,
|
|
||||||
groupID string, operations []GroupUpdateOperation,
|
|
||||||
) (*Group, error) {
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupToUpdate, ok := account.Groups[groupID]
|
|
||||||
if !ok {
|
|
||||||
return nil, status.Errorf(status.NotFound, "group with ID %s no longer exists", groupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
group := groupToUpdate.Copy()
|
|
||||||
|
|
||||||
for _, operation := range operations {
|
|
||||||
switch operation.Type {
|
|
||||||
case UpdateGroupName:
|
|
||||||
group.Name = operation.Values[0]
|
|
||||||
case UpdateGroupPeers:
|
|
||||||
group.Peers = operation.Values
|
|
||||||
case InsertPeersToGroup:
|
|
||||||
sourceList := group.Peers
|
|
||||||
resultList := removeFromList(sourceList, operation.Values)
|
|
||||||
group.Peers = append(resultList, operation.Values...)
|
|
||||||
case RemovePeersFromGroup:
|
|
||||||
sourceList := group.Peers
|
|
||||||
resultList := removeFromList(sourceList, operation.Values)
|
|
||||||
group.Peers = resultList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
account.Groups[groupID] = group
|
|
||||||
|
|
||||||
account.Network.IncSerial()
|
|
||||||
if err = am.Store.SaveAccount(account); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return group, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteGroup object of the peers
|
// DeleteGroup object of the peers
|
||||||
func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) error {
|
func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) error {
|
||||||
unlock := am.Store.AcquireAccountLock(accountId)
|
unlock := am.Store.AcquireAccountLock(accountId)
|
||||||
@@ -300,7 +226,9 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string)
|
|||||||
|
|
||||||
am.storeEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta())
|
am.storeEvent(userId, groupID, accountId, activity.GroupDeleted, g.EventMeta())
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroups objects of the peers
|
// ListGroups objects of the peers
|
||||||
@@ -352,11 +280,13 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupDeletePeer removes peer from the group
|
// GroupDeletePeer removes peer from the group
|
||||||
func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error {
|
func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
@@ -372,7 +302,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey str
|
|||||||
|
|
||||||
account.Network.IncSerial()
|
account.Network.IncSerial()
|
||||||
for i, itemID := range group.Peers {
|
for i, itemID := range group.Peers {
|
||||||
if itemID == peerKey {
|
if itemID == peerID {
|
||||||
group.Peers = append(group.Peers[:i], group.Peers[i+1:]...)
|
group.Peers = append(group.Peers[:i], group.Peers[i+1:]...)
|
||||||
if err := am.Store.SaveAccount(account); err != nil {
|
if err := am.Store.SaveAccount(account); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -380,31 +310,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
}
|
|
||||||
|
return nil
|
||||||
// GroupListPeers returns list of the peers from the group
|
|
||||||
func (am *DefaultAccountManager) GroupListPeers(accountID, groupID string) ([]*Peer, error) {
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.NotFound, "account not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
group, ok := account.Groups[groupID]
|
|
||||||
if !ok {
|
|
||||||
return nil, status.Errorf(status.NotFound, "group with ID %s not found", groupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
peers := make([]*Peer, 0, len(account.Groups))
|
|
||||||
for _, peerID := range group.Peers {
|
|
||||||
p, ok := account.Peers[peerID]
|
|
||||||
if ok {
|
|
||||||
peers = append(peers, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return peers, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi
|
|||||||
select {
|
select {
|
||||||
// condition when there are some updates
|
// condition when there are some updates
|
||||||
case update, open := <-updates:
|
case update, open := <-updates:
|
||||||
|
|
||||||
|
if s.appMetrics != nil {
|
||||||
|
s.appMetrics.GRPCMetrics().UpdateChannelQueueLength(len(updates) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
if !open {
|
if !open {
|
||||||
log.Debugf("updates channel for peer %s was closed", peerKey.String())
|
log.Debugf("updates channel for peer %s was closed", peerKey.String())
|
||||||
s.cancelPeerRoutines(peer)
|
s.cancelPeerRoutines(peer)
|
||||||
|
|||||||
0
management/server/http/api/generate.sh
Normal file → Executable file
0
management/server/http/api/generate.sh
Normal file → Executable file
@@ -745,9 +745,15 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
example: true
|
example: true
|
||||||
peer:
|
peer:
|
||||||
description: Peer Identifier associated with route
|
description: Peer Identifier associated with route. This property can not be set together with `peer_groups`
|
||||||
type: string
|
type: string
|
||||||
example: chacbco6lnnbn6cg5s91
|
example: chacbco6lnnbn6cg5s91
|
||||||
|
peer_groups:
|
||||||
|
description: Peers Group Identifier associated with route. This property can not be set together with `peer`
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
example: chacbco6lnnbn6cg5s91
|
||||||
network:
|
network:
|
||||||
description: Network range in CIDR format
|
description: Network range in CIDR format
|
||||||
type: string
|
type: string
|
||||||
@@ -773,7 +779,9 @@ components:
|
|||||||
- description
|
- description
|
||||||
- network_id
|
- network_id
|
||||||
- enabled
|
- enabled
|
||||||
- peer
|
# Only one property has to be set
|
||||||
|
#- peer
|
||||||
|
#- peer_groups
|
||||||
- network
|
- network
|
||||||
- metric
|
- metric
|
||||||
- masquerade
|
- masquerade
|
||||||
@@ -922,6 +930,14 @@ components:
|
|||||||
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
description: The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
||||||
type: string
|
type: string
|
||||||
example: google-oauth2|123456789012345678901
|
example: google-oauth2|123456789012345678901
|
||||||
|
initiator_name:
|
||||||
|
description: The name of the initiator of the event.
|
||||||
|
type: string
|
||||||
|
example: John Doe
|
||||||
|
initiator_email:
|
||||||
|
description: The e-mail address of the initiator of the event. E.g., an e-mail of a user that triggered the event.
|
||||||
|
type: string
|
||||||
|
example: demo@netbird.io
|
||||||
target_id:
|
target_id:
|
||||||
description: The ID of the target of the event. E.g., an ID of the peer that a user removed.
|
description: The ID of the target of the event. E.g., an ID of the peer that a user removed.
|
||||||
type: string
|
type: string
|
||||||
@@ -938,6 +954,8 @@ components:
|
|||||||
- activity
|
- activity
|
||||||
- activity_code
|
- activity_code
|
||||||
- initiator_id
|
- initiator_id
|
||||||
|
- initiator_name
|
||||||
|
- initiator_email
|
||||||
- target_id
|
- target_id
|
||||||
- meta
|
- meta
|
||||||
responses:
|
responses:
|
||||||
@@ -1134,8 +1152,8 @@ paths:
|
|||||||
'500':
|
'500':
|
||||||
"$ref": "#/components/responses/internal_error"
|
"$ref": "#/components/responses/internal_error"
|
||||||
delete:
|
delete:
|
||||||
summary: Block a User
|
summary: Delete a User
|
||||||
description: This method blocks a user from accessing the system, but leaves the IDP user intact.
|
description: This method removes a user from accessing the system. For this leaves the IDP user intact unless the `--user-delete-from-idp` is passed to management startup.
|
||||||
tags: [ Users ]
|
tags: [ Users ]
|
||||||
security:
|
security:
|
||||||
- BearerAuth: [ ]
|
- BearerAuth: [ ]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Package api provides primitives to interact with the openapi HTTP API.
|
// Package api provides primitives to interact with the openapi HTTP API.
|
||||||
//
|
//
|
||||||
// Code generated by github.com/deepmap/oapi-codegen version v1.11.1-0.20220912230023-4a1477f6a8ba DO NOT EDIT.
|
// Code generated by github.com/deepmap/oapi-codegen version v1.15.0 DO NOT EDIT.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -164,9 +164,15 @@ type Event struct {
|
|||||||
// Id Event unique identifier
|
// Id Event unique identifier
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
|
||||||
|
// InitiatorEmail The e-mail address of the initiator of the event. E.g., an e-mail of a user that triggered the event.
|
||||||
|
InitiatorEmail string `json:"initiator_email"`
|
||||||
|
|
||||||
// InitiatorId The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
// InitiatorId The ID of the initiator of the event. E.g., an ID of a user that triggered the event.
|
||||||
InitiatorId string `json:"initiator_id"`
|
InitiatorId string `json:"initiator_id"`
|
||||||
|
|
||||||
|
// InitiatorName The name of the initiator of the event.
|
||||||
|
InitiatorName string `json:"initiator_name"`
|
||||||
|
|
||||||
// Meta The metadata of the event
|
// Meta The metadata of the event
|
||||||
Meta map[string]string `json:"meta"`
|
Meta map[string]string `json:"meta"`
|
||||||
|
|
||||||
@@ -593,8 +599,11 @@ type Route struct {
|
|||||||
// NetworkType Network type indicating if it is IPv4 or IPv6
|
// NetworkType Network type indicating if it is IPv4 or IPv6
|
||||||
NetworkType string `json:"network_type"`
|
NetworkType string `json:"network_type"`
|
||||||
|
|
||||||
// Peer Peer Identifier associated with route
|
// Peer Peer Identifier associated with route. This property can not be set together with `peer_groups`
|
||||||
Peer string `json:"peer"`
|
Peer *string `json:"peer,omitempty"`
|
||||||
|
|
||||||
|
// PeerGroups Peers Group Identifier associated with route. This property can not be set together with `peer`
|
||||||
|
PeerGroups *[]string `json:"peer_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteRequest defines model for RouteRequest.
|
// RouteRequest defines model for RouteRequest.
|
||||||
@@ -620,8 +629,11 @@ type RouteRequest struct {
|
|||||||
// NetworkId Route network identifier, to group HA routes
|
// NetworkId Route network identifier, to group HA routes
|
||||||
NetworkId string `json:"network_id"`
|
NetworkId string `json:"network_id"`
|
||||||
|
|
||||||
// Peer Peer Identifier associated with route
|
// Peer Peer Identifier associated with route. This property can not be set together with `peer_groups`
|
||||||
Peer string `json:"peer"`
|
Peer *string `json:"peer,omitempty"`
|
||||||
|
|
||||||
|
// PeerGroups Peers Group Identifier associated with route. This property can not be set together with `peer`
|
||||||
|
PeerGroups *[]string `json:"peer_groups,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule defines model for Rule.
|
// Rule defines model for Rule.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
testDNSSettingsUserID = "test_user"
|
testDNSSettingsUserID = "test_user"
|
||||||
)
|
)
|
||||||
|
|
||||||
var baseExistingDNSSettings = &server.DNSSettings{
|
var baseExistingDNSSettings = server.DNSSettings{
|
||||||
DisabledManagementGroups: []string{testDNSSettingsExistingGroup},
|
DisabledManagementGroups: []string{testDNSSettingsExistingGroup},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ func initDNSSettingsTestData() *DNSSettingsHandler {
|
|||||||
return &DNSSettingsHandler{
|
return &DNSSettingsHandler{
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
GetDNSSettingsFunc: func(accountID string, userID string) (*server.DNSSettings, error) {
|
GetDNSSettingsFunc: func(accountID string, userID string) (*server.DNSSettings, error) {
|
||||||
return testingDNSSettingsAccount.DNSSettings, nil
|
return &testingDNSSettingsAccount.DNSSettings, nil
|
||||||
},
|
},
|
||||||
SaveDNSSettingsFunc: func(accountID string, userID string, dnsSettingsToSave *server.DNSSettings) error {
|
SaveDNSSettingsFunc: func(accountID string, userID string, dnsSettingsToSave *server.DNSSettings) error {
|
||||||
if dnsSettingsToSave != nil {
|
if dnsSettingsToSave != nil {
|
||||||
|
|||||||
@@ -45,14 +45,66 @@ func (h *EventsHandler) GetAllEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
events := make([]*api.Event, 0)
|
events := make([]*api.Event, len(accountEvents))
|
||||||
for _, e := range accountEvents {
|
for i, e := range accountEvents {
|
||||||
events = append(events, toEventResponse(e))
|
events[i] = toEventResponse(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.fillEventsWithUserInfo(events, account.Id, user.Id)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(err, w)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.WriteJSONObject(w, events)
|
util.WriteJSONObject(w, events)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *EventsHandler) fillEventsWithUserInfo(events []*api.Event, accountId, userId string) error {
|
||||||
|
// build email, name maps based on users
|
||||||
|
userInfos, err := h.accountManager.GetUsersFromAccount(accountId, userId)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to get users from account: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
emails := make(map[string]string)
|
||||||
|
names := make(map[string]string)
|
||||||
|
for _, ui := range userInfos {
|
||||||
|
emails[ui.ID] = ui.Email
|
||||||
|
names[ui.ID] = ui.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
for _, event := range events {
|
||||||
|
// fill initiator
|
||||||
|
if event.InitiatorEmail == "" {
|
||||||
|
event.InitiatorEmail, ok = emails[event.InitiatorId]
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("failed to resolve email for initiator: %s", event.InitiatorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.InitiatorName == "" {
|
||||||
|
// here to allowed to be empty because in the first release we did not store the name
|
||||||
|
event.InitiatorName = names[event.InitiatorId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill target meta
|
||||||
|
email, ok := emails[event.TargetId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
event.Meta["email"] = email
|
||||||
|
|
||||||
|
username, ok := names[event.TargetId]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
event.Meta["username"] = username
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func toEventResponse(event *activity.Event) *api.Event {
|
func toEventResponse(event *activity.Event) *api.Event {
|
||||||
meta := make(map[string]string)
|
meta := make(map[string]string)
|
||||||
if event.Meta != nil {
|
if event.Meta != nil {
|
||||||
@@ -60,13 +112,16 @@ func toEventResponse(event *activity.Event) *api.Event {
|
|||||||
meta[s] = fmt.Sprintf("%v", a)
|
meta[s] = fmt.Sprintf("%v", a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &api.Event{
|
e := &api.Event{
|
||||||
Id: fmt.Sprint(event.ID),
|
Id: fmt.Sprint(event.ID),
|
||||||
InitiatorId: event.InitiatorID,
|
InitiatorId: event.InitiatorID,
|
||||||
Activity: event.Activity.Message(),
|
InitiatorName: event.InitiatorName,
|
||||||
ActivityCode: api.EventActivityCode(event.Activity.StringCode()),
|
InitiatorEmail: event.InitiatorEmail,
|
||||||
TargetId: event.TargetID,
|
Activity: event.Activity.Message(),
|
||||||
Timestamp: event.Timestamp,
|
ActivityCode: api.EventActivityCode(event.Activity.StringCode()),
|
||||||
Meta: meta,
|
TargetId: event.TargetID,
|
||||||
|
Timestamp: event.Timestamp,
|
||||||
|
Meta: meta,
|
||||||
}
|
}
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ func initEventsTestData(account string, user *server.User, events ...*activity.E
|
|||||||
},
|
},
|
||||||
}, user, nil
|
}, user, nil
|
||||||
},
|
},
|
||||||
|
GetUsersFromAccountFunc: func(accountID, userID string) ([]*server.UserInfo, error) {
|
||||||
|
return make([]*server.UserInfo, 0), nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||||
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
||||||
|
|||||||
@@ -53,30 +53,6 @@ func initGroupTestData(user *server.User, groups ...*server.Group) *GroupsHandle
|
|||||||
Issued: server.GroupIssuedAPI,
|
Issued: server.GroupIssuedAPI,
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
UpdateGroupFunc: func(_ string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) {
|
|
||||||
var group server.Group
|
|
||||||
group.ID = groupID
|
|
||||||
for _, operation := range operations {
|
|
||||||
switch operation.Type {
|
|
||||||
case server.UpdateGroupName:
|
|
||||||
group.Name = operation.Values[0]
|
|
||||||
case server.UpdateGroupPeers, server.InsertPeersToGroup:
|
|
||||||
group.Peers = operation.Values
|
|
||||||
case server.RemovePeersFromGroup:
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("no operation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &group, nil
|
|
||||||
},
|
|
||||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
|
||||||
for _, peer := range TestPeers {
|
|
||||||
if peer.IP.String() == peerIP {
|
|
||||||
return peer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("peer not found")
|
|
||||||
},
|
|
||||||
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||||
return &server.Account{
|
return &server.Account{
|
||||||
Id: claims.AccountId,
|
Id: claims.AccountId,
|
||||||
|
|||||||
@@ -88,31 +88,6 @@ func initNameserversTestData() *NameserversHandler {
|
|||||||
}
|
}
|
||||||
return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
|
return status.Errorf(status.NotFound, "nameserver group with ID %s was not found", nsGroupToSave.ID)
|
||||||
},
|
},
|
||||||
UpdateNameServerGroupFunc: func(accountID, nsGroupID, _ string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
|
||||||
nsGroupToUpdate := baseExistingNSGroup.Copy()
|
|
||||||
if nsGroupID != nsGroupToUpdate.ID {
|
|
||||||
return nil, status.Errorf(status.NotFound, "nameserver group ID %s no longer exists", nsGroupID)
|
|
||||||
}
|
|
||||||
for _, operation := range operations {
|
|
||||||
switch operation.Type {
|
|
||||||
case server.UpdateNameServerGroupName:
|
|
||||||
nsGroupToUpdate.Name = operation.Values[0]
|
|
||||||
case server.UpdateNameServerGroupDescription:
|
|
||||||
nsGroupToUpdate.Description = operation.Values[0]
|
|
||||||
case server.UpdateNameServerGroupNameServers:
|
|
||||||
var parsedNSList []nbdns.NameServer
|
|
||||||
for _, nsURL := range operation.Values {
|
|
||||||
parsed, err := nbdns.ParseNameServerURL(nsURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
parsedNSList = append(parsedNSList, parsed)
|
|
||||||
}
|
|
||||||
nsGroupToUpdate.NameServers = parsedNSList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nsGroupToUpdate, nil
|
|
||||||
},
|
|
||||||
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||||
return testingNSAccount, testingAccount.Users["test_user"], nil
|
return testingNSAccount, testingAccount.Users["test_user"], nil
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,24 @@ func NewPeersHandler(accountManager server.AccountManager, authCfg AuthCfg) *Pee
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *PeersHandler) checkPeerStatus(peer *server.Peer) (*server.Peer, error) {
|
||||||
|
peerToReturn := peer.Copy()
|
||||||
|
if peer.Status.Connected {
|
||||||
|
statuses, err := h.accountManager.GetAllConnectedPeers()
|
||||||
|
if err != nil {
|
||||||
|
return peerToReturn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although we have online status in store we do not yet have an updated channel so have to show it as disconnected
|
||||||
|
// This may happen after server restart when not all peers are yet connected
|
||||||
|
if _, connected := statuses[peerToReturn.ID]; !connected {
|
||||||
|
peerToReturn.Status.Connected = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peerToReturn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w http.ResponseWriter) {
|
func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w http.ResponseWriter) {
|
||||||
peer, err := h.accountManager.GetPeer(account.Id, peerID, userID)
|
peer, err := h.accountManager.GetPeer(account.Id, peerID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,7 +56,13 @@ func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
util.WriteJSONObject(w, toPeerResponse(peer, account, h.accountManager.GetDNSDomain()))
|
peerToReturn, err := h.checkPeerStatus(peer)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
util.WriteJSONObject(w, toPeerResponse(peerToReturn, account, h.accountManager.GetDNSDomain()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, peerID string, w http.ResponseWriter, r *http.Request) {
|
func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, peerID string, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -61,7 +85,7 @@ func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, pe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *PeersHandler) deletePeer(accountID, userID string, peerID string, w http.ResponseWriter) {
|
func (h *PeersHandler) deletePeer(accountID, userID string, peerID string, w http.ResponseWriter) {
|
||||||
_, err := h.accountManager.DeletePeer(accountID, peerID, userID)
|
err := h.accountManager.DeletePeer(accountID, peerID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
return
|
return
|
||||||
@@ -120,7 +144,12 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
respBody := []*api.Peer{}
|
respBody := []*api.Peer{}
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
respBody = append(respBody, toPeerResponse(peer, account, dnsDomain))
|
peerToReturn, err := h.checkPeerStatus(peer)
|
||||||
|
if err != nil {
|
||||||
|
util.WriteError(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respBody = append(respBody, toPeerResponse(peerToReturn, account, dnsDomain))
|
||||||
}
|
}
|
||||||
util.WriteJSONObject(w, respBody)
|
util.WriteJSONObject(w, respBody)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,19 +24,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const testPeerID = "test_peer"
|
const testPeerID = "test_peer"
|
||||||
|
const noUpdateChannelTestPeerID = "no-update-channel"
|
||||||
|
|
||||||
func initTestMetaData(peers ...*server.Peer) *PeersHandler {
|
func initTestMetaData(peers ...*server.Peer) *PeersHandler {
|
||||||
return &PeersHandler{
|
return &PeersHandler{
|
||||||
accountManager: &mock_server.MockAccountManager{
|
accountManager: &mock_server.MockAccountManager{
|
||||||
UpdatePeerFunc: func(accountID, userID string, update *server.Peer) (*server.Peer, error) {
|
UpdatePeerFunc: func(accountID, userID string, update *server.Peer) (*server.Peer, error) {
|
||||||
p := peers[0].Copy()
|
var p *server.Peer
|
||||||
|
for _, peer := range peers {
|
||||||
|
if update.ID == peer.ID {
|
||||||
|
p = peer.Copy()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
p.SSHEnabled = update.SSHEnabled
|
p.SSHEnabled = update.SSHEnabled
|
||||||
p.LoginExpirationEnabled = update.LoginExpirationEnabled
|
p.LoginExpirationEnabled = update.LoginExpirationEnabled
|
||||||
p.Name = update.Name
|
p.Name = update.Name
|
||||||
return p, nil
|
return p, nil
|
||||||
},
|
},
|
||||||
GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) {
|
GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) {
|
||||||
return peers[0], nil
|
var p *server.Peer
|
||||||
|
for _, peer := range peers {
|
||||||
|
if peerID == peer.ID {
|
||||||
|
p = peer.Copy()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
},
|
},
|
||||||
GetPeersFunc: func(accountID, userID string) ([]*server.Peer, error) {
|
GetPeersFunc: func(accountID, userID string) ([]*server.Peer, error) {
|
||||||
return peers, nil
|
return peers, nil
|
||||||
@@ -57,6 +72,16 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler {
|
|||||||
},
|
},
|
||||||
}, user, nil
|
}, user, nil
|
||||||
},
|
},
|
||||||
|
GetAllConnectedPeersFunc: func() (map[string]struct{}, error) {
|
||||||
|
statuses := make(map[string]struct{})
|
||||||
|
for _, peer := range peers {
|
||||||
|
if peer.ID == noUpdateChannelTestPeerID {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
statuses[peer.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
return statuses, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
||||||
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
||||||
@@ -79,7 +104,7 @@ func TestGetPeers(t *testing.T) {
|
|||||||
Key: "key",
|
Key: "key",
|
||||||
SetupKey: "setupkey",
|
SetupKey: "setupkey",
|
||||||
IP: net.ParseIP("100.64.0.1"),
|
IP: net.ParseIP("100.64.0.1"),
|
||||||
Status: &server.PeerStatus{},
|
Status: &server.PeerStatus{Connected: true},
|
||||||
Name: "PeerName",
|
Name: "PeerName",
|
||||||
LoginExpirationEnabled: false,
|
LoginExpirationEnabled: false,
|
||||||
Meta: server.PeerSystemMeta{
|
Meta: server.PeerSystemMeta{
|
||||||
@@ -93,11 +118,17 @@ func TestGetPeers(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peer1 := peer.Copy()
|
||||||
|
peer1.ID = noUpdateChannelTestPeerID
|
||||||
|
|
||||||
expectedUpdatedPeer := peer.Copy()
|
expectedUpdatedPeer := peer.Copy()
|
||||||
expectedUpdatedPeer.LoginExpirationEnabled = true
|
expectedUpdatedPeer.LoginExpirationEnabled = true
|
||||||
expectedUpdatedPeer.SSHEnabled = true
|
expectedUpdatedPeer.SSHEnabled = true
|
||||||
expectedUpdatedPeer.Name = "New Name"
|
expectedUpdatedPeer.Name = "New Name"
|
||||||
|
|
||||||
|
expectedPeer1 := peer1.Copy()
|
||||||
|
expectedPeer1.Status.Connected = false
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
expectedStatus int
|
expectedStatus int
|
||||||
@@ -116,13 +147,21 @@ func TestGetPeers(t *testing.T) {
|
|||||||
expectedPeer: peer,
|
expectedPeer: peer,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "GetPeer",
|
name: "GetPeer with update channel",
|
||||||
requestType: http.MethodGet,
|
requestType: http.MethodGet,
|
||||||
requestPath: "/api/peers/" + testPeerID,
|
requestPath: "/api/peers/" + testPeerID,
|
||||||
expectedStatus: http.StatusOK,
|
expectedStatus: http.StatusOK,
|
||||||
expectedArray: false,
|
expectedArray: false,
|
||||||
expectedPeer: peer,
|
expectedPeer: peer,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "GetPeer with no update channel",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/peers/" + peer1.ID,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedArray: false,
|
||||||
|
expectedPeer: expectedPeer1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "PutPeer",
|
name: "PutPeer",
|
||||||
requestType: http.MethodPut,
|
requestType: http.MethodPut,
|
||||||
@@ -136,7 +175,7 @@ func TestGetPeers(t *testing.T) {
|
|||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
p := initTestMetaData(peer)
|
p := initTestMetaData(peer, peer1)
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -171,6 +210,10 @@ func TestGetPeers(t *testing.T) {
|
|||||||
t.Fatalf("Sent content is not in correct json format; %v", err)
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hardcode this check for now as we only have two peers in this suite
|
||||||
|
assert.Equal(t, len(respBody), 2)
|
||||||
|
assert.Equal(t, respBody[1].Connected, false)
|
||||||
|
|
||||||
got = respBody[0]
|
got = respBody[0]
|
||||||
} else {
|
} else {
|
||||||
got = &api.Peer{}
|
got = &api.Peer{}
|
||||||
@@ -180,12 +223,15 @@ func TestGetPeers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println(got)
|
||||||
|
|
||||||
assert.Equal(t, got.Name, tc.expectedPeer.Name)
|
assert.Equal(t, got.Name, tc.expectedPeer.Name)
|
||||||
assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion)
|
assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion)
|
||||||
assert.Equal(t, got.Ip, tc.expectedPeer.IP.String())
|
assert.Equal(t, got.Ip, tc.expectedPeer.IP.String())
|
||||||
assert.Equal(t, got.Os, "OS core")
|
assert.Equal(t, got.Os, "OS core")
|
||||||
assert.Equal(t, got.LoginExpirationEnabled, tc.expectedPeer.LoginExpirationEnabled)
|
assert.Equal(t, got.LoginExpirationEnabled, tc.expectedPeer.LoginExpirationEnabled)
|
||||||
assert.Equal(t, got.SshEnabled, tc.expectedPeer.SSHEnabled)
|
assert.Equal(t, got.SshEnabled, tc.expectedPeer.SSHEnabled)
|
||||||
|
assert.Equal(t, got.Connected, tc.expectedPeer.Status.Connected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,33 @@ func (h *RoutesHandler) CreateRoute(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), req.Peer, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id)
|
peerId := ""
|
||||||
|
if req.Peer != nil {
|
||||||
|
peerId = *req.Peer
|
||||||
|
}
|
||||||
|
|
||||||
|
peerGroupIds := []string{}
|
||||||
|
if req.PeerGroups != nil {
|
||||||
|
peerGroupIds = *req.PeerGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerId != "" && len(peerGroupIds) > 0) || (peerId == "" && len(peerGroupIds) == 0) {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "only one peer or peer_groups should be provided"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not allow non Linux peers
|
||||||
|
if peer := account.GetPeer(peerId); peer != nil {
|
||||||
|
if peer.Meta.GoOS != "linux" {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are non supported as network routes"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newRoute, err := h.accountManager.CreateRoute(
|
||||||
|
account.Id, newPrefix.String(), peerId, peerGroupIds,
|
||||||
|
req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
return
|
return
|
||||||
@@ -135,19 +161,49 @@ func (h *RoutesHandler) UpdateRoute(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Peer != nil && req.PeerGroups != nil {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "only peer or peers_group should be provided"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Peer == nil && req.PeerGroups == nil {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "either peer or peers_group should be provided"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peerID := ""
|
||||||
|
if req.Peer != nil {
|
||||||
|
peerID = *req.Peer
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not allow non Linux peers
|
||||||
|
if peer := account.GetPeer(peerID); peer != nil {
|
||||||
|
if peer.Meta.GoOS != "linux" {
|
||||||
|
util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are non supported as network routes"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newRoute := &route.Route{
|
newRoute := &route.Route{
|
||||||
ID: routeID,
|
ID: routeID,
|
||||||
Network: newPrefix,
|
Network: newPrefix,
|
||||||
NetID: req.NetworkId,
|
NetID: req.NetworkId,
|
||||||
NetworkType: prefixType,
|
NetworkType: prefixType,
|
||||||
Masquerade: req.Masquerade,
|
Masquerade: req.Masquerade,
|
||||||
Peer: req.Peer,
|
|
||||||
Metric: req.Metric,
|
Metric: req.Metric,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Enabled: req.Enabled,
|
Enabled: req.Enabled,
|
||||||
Groups: req.Groups,
|
Groups: req.Groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Peer != nil {
|
||||||
|
newRoute.Peer = peerID
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.PeerGroups != nil {
|
||||||
|
newRoute.PeerGroups = *req.PeerGroups
|
||||||
|
}
|
||||||
|
|
||||||
err = h.accountManager.SaveRoute(account.Id, user.Id, newRoute)
|
err = h.accountManager.SaveRoute(account.Id, user.Id, newRoute)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.WriteError(err, w)
|
util.WriteError(err, w)
|
||||||
@@ -208,16 +264,21 @@ func (h *RoutesHandler) GetRoute(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toRouteResponse(serverRoute *route.Route) *api.Route {
|
func toRouteResponse(serverRoute *route.Route) *api.Route {
|
||||||
return &api.Route{
|
route := &api.Route{
|
||||||
Id: serverRoute.ID,
|
Id: serverRoute.ID,
|
||||||
Description: serverRoute.Description,
|
Description: serverRoute.Description,
|
||||||
NetworkId: serverRoute.NetID,
|
NetworkId: serverRoute.NetID,
|
||||||
Enabled: serverRoute.Enabled,
|
Enabled: serverRoute.Enabled,
|
||||||
Peer: serverRoute.Peer,
|
Peer: &serverRoute.Peer,
|
||||||
Network: serverRoute.Network.String(),
|
Network: serverRoute.Network.String(),
|
||||||
NetworkType: serverRoute.NetworkType.String(),
|
NetworkType: serverRoute.NetworkType.String(),
|
||||||
Masquerade: serverRoute.Masquerade,
|
Masquerade: serverRoute.Masquerade,
|
||||||
Metric: serverRoute.Metric,
|
Metric: serverRoute.Metric,
|
||||||
Groups: serverRoute.Groups,
|
Groups: serverRoute.Groups,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(serverRoute.PeerGroups) > 0 {
|
||||||
|
route.PeerGroups = &serverRoute.PeerGroups
|
||||||
|
}
|
||||||
|
return route
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/http/api"
|
"github.com/netbirdio/netbird/management/server/http/api"
|
||||||
@@ -24,16 +23,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
existingRouteID = "existingRouteID"
|
existingRouteID = "existingRouteID"
|
||||||
notFoundRouteID = "notFoundRouteID"
|
existingRouteID2 = "existingRouteID2" // for peer_groups test
|
||||||
existingPeerIP = "100.64.0.100"
|
notFoundRouteID = "notFoundRouteID"
|
||||||
existingPeerID = "peer-id"
|
existingPeerIP1 = "100.64.0.100"
|
||||||
notFoundPeerID = "nonExistingPeer"
|
existingPeerIP2 = "100.64.0.101"
|
||||||
existingPeerKey = "existingPeerKey"
|
notFoundPeerID = "nonExistingPeer"
|
||||||
testAccountID = "test_id"
|
existingPeerKey = "existingPeerKey"
|
||||||
existingGroupID = "testGroup"
|
nonLinuxExistingPeerKey = "darwinExistingPeerKey"
|
||||||
|
testAccountID = "test_id"
|
||||||
|
existingGroupID = "testGroup"
|
||||||
|
notFoundGroupID = "nonExistingGroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var emptyString = ""
|
||||||
|
var existingPeerID = "peer-id"
|
||||||
|
var nonLinuxExistingPeerID = "darwin-peer-id"
|
||||||
|
|
||||||
var baseExistingRoute = &route.Route{
|
var baseExistingRoute = &route.Route{
|
||||||
ID: existingRouteID,
|
ID: existingRouteID,
|
||||||
Description: "base route",
|
Description: "base route",
|
||||||
@@ -52,8 +58,19 @@ var testingAccount = &server.Account{
|
|||||||
Peers: map[string]*server.Peer{
|
Peers: map[string]*server.Peer{
|
||||||
existingPeerID: {
|
existingPeerID: {
|
||||||
Key: existingPeerKey,
|
Key: existingPeerKey,
|
||||||
IP: netip.MustParseAddr(existingPeerIP).AsSlice(),
|
IP: netip.MustParseAddr(existingPeerIP1).AsSlice(),
|
||||||
ID: existingPeerID,
|
ID: existingPeerID,
|
||||||
|
Meta: server.PeerSystemMeta{
|
||||||
|
GoOS: "linux",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nonLinuxExistingPeerID: {
|
||||||
|
Key: nonLinuxExistingPeerID,
|
||||||
|
IP: netip.MustParseAddr(existingPeerIP2).AsSlice(),
|
||||||
|
ID: nonLinuxExistingPeerID,
|
||||||
|
Meta: server.PeerSystemMeta{
|
||||||
|
GoOS: "darwin",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Users: map[string]*server.User{
|
Users: map[string]*server.User{
|
||||||
@@ -68,17 +85,26 @@ func initRoutesTestData() *RoutesHandler {
|
|||||||
if routeID == existingRouteID {
|
if routeID == existingRouteID {
|
||||||
return baseExistingRoute, nil
|
return baseExistingRoute, nil
|
||||||
}
|
}
|
||||||
|
if routeID == existingRouteID2 {
|
||||||
|
route := baseExistingRoute.Copy()
|
||||||
|
route.PeerGroups = []string{existingGroupID}
|
||||||
|
return route, nil
|
||||||
|
}
|
||||||
return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
|
return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
|
||||||
},
|
},
|
||||||
CreateRouteFunc: func(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) {
|
CreateRouteFunc: func(accountID, network, peerID string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) {
|
||||||
if peerID == notFoundPeerID {
|
if peerID == notFoundPeerID {
|
||||||
return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
||||||
}
|
}
|
||||||
|
if len(peerGroups) > 0 && peerGroups[0] == notFoundGroupID {
|
||||||
|
return nil, status.Errorf(status.InvalidArgument, "peer groups with ID %s not found", peerGroups[0])
|
||||||
|
}
|
||||||
networkType, p, _ := route.ParseNetwork(network)
|
networkType, p, _ := route.ParseNetwork(network)
|
||||||
return &route.Route{
|
return &route.Route{
|
||||||
ID: existingRouteID,
|
ID: existingRouteID,
|
||||||
NetID: netID,
|
NetID: netID,
|
||||||
Peer: peerID,
|
Peer: peerID,
|
||||||
|
PeerGroups: peerGroups,
|
||||||
Network: p,
|
Network: p,
|
||||||
NetworkType: networkType,
|
NetworkType: networkType,
|
||||||
Description: description,
|
Description: description,
|
||||||
@@ -99,47 +125,6 @@ func initRoutesTestData() *RoutesHandler {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) {
|
|
||||||
if peerIP != existingPeerID {
|
|
||||||
return nil, status.Errorf(status.NotFound, "Peer with ID %s not found", peerIP)
|
|
||||||
}
|
|
||||||
return &server.Peer{
|
|
||||||
Key: existingPeerKey,
|
|
||||||
IP: netip.MustParseAddr(existingPeerID).AsSlice(),
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
UpdateRouteFunc: func(_ string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) {
|
|
||||||
routeToUpdate := baseExistingRoute
|
|
||||||
if routeID != routeToUpdate.ID {
|
|
||||||
return nil, status.Errorf(status.NotFound, "route %s no longer exists", routeID)
|
|
||||||
}
|
|
||||||
for _, operation := range operations {
|
|
||||||
switch operation.Type {
|
|
||||||
case server.UpdateRouteNetwork:
|
|
||||||
routeToUpdate.NetworkType, routeToUpdate.Network, _ = route.ParseNetwork(operation.Values[0])
|
|
||||||
case server.UpdateRouteDescription:
|
|
||||||
routeToUpdate.Description = operation.Values[0]
|
|
||||||
case server.UpdateRouteNetworkIdentifier:
|
|
||||||
routeToUpdate.NetID = operation.Values[0]
|
|
||||||
case server.UpdateRoutePeer:
|
|
||||||
routeToUpdate.Peer = operation.Values[0]
|
|
||||||
if routeToUpdate.Peer == notFoundPeerID {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", routeToUpdate.Peer)
|
|
||||||
}
|
|
||||||
case server.UpdateRouteMetric:
|
|
||||||
routeToUpdate.Metric, _ = strconv.Atoi(operation.Values[0])
|
|
||||||
case server.UpdateRouteMasquerade:
|
|
||||||
routeToUpdate.Masquerade, _ = strconv.ParseBool(operation.Values[0])
|
|
||||||
case server.UpdateRouteEnabled:
|
|
||||||
routeToUpdate.Enabled, _ = strconv.ParseBool(operation.Values[0])
|
|
||||||
case server.UpdateRouteGroups:
|
|
||||||
routeToUpdate.Groups = operation.Values
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("no operation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return routeToUpdate, nil
|
|
||||||
},
|
|
||||||
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
GetAccountFromTokenFunc: func(_ jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||||
return testingAccount, testingAccount.Users["test_user"], nil
|
return testingAccount, testingAccount.Users["test_user"], nil
|
||||||
},
|
},
|
||||||
@@ -157,6 +142,9 @@ func initRoutesTestData() *RoutesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRoutesHandlers(t *testing.T) {
|
func TestRoutesHandlers(t *testing.T) {
|
||||||
|
baseExistingRouteWithPeerGroups := baseExistingRoute.Copy()
|
||||||
|
baseExistingRouteWithPeerGroups.PeerGroups = []string{existingGroupID}
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
expectedStatus int
|
expectedStatus int
|
||||||
@@ -180,6 +168,14 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
requestPath: "/api/routes/" + notFoundRouteID,
|
requestPath: "/api/routes/" + notFoundRouteID,
|
||||||
expectedStatus: http.StatusNotFound,
|
expectedStatus: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Get Existing Route with Peer Groups",
|
||||||
|
requestType: http.MethodGet,
|
||||||
|
requestPath: "/api/routes/" + existingRouteID2,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedRoute: toRouteResponse(baseExistingRouteWithPeerGroups),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Delete Existing Route",
|
name: "Delete Existing Route",
|
||||||
requestType: http.MethodDelete,
|
requestType: http.MethodDelete,
|
||||||
@@ -206,13 +202,21 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
Description: "Post",
|
Description: "Post",
|
||||||
NetworkId: "awesomeNet",
|
NetworkId: "awesomeNet",
|
||||||
Network: "192.168.0.0/16",
|
Network: "192.168.0.0/16",
|
||||||
Peer: existingPeerID,
|
Peer: &existingPeerID,
|
||||||
NetworkType: route.IPv4NetworkString,
|
NetworkType: route.IPv4NetworkString,
|
||||||
Masquerade: false,
|
Masquerade: false,
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Groups: []string{existingGroupID},
|
Groups: []string{existingGroupID},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "POST Non Linux Peer",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/routes",
|
||||||
|
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "POST Not Found Peer",
|
name: "POST Not Found Peer",
|
||||||
requestType: http.MethodPost,
|
requestType: http.MethodPost,
|
||||||
@@ -237,6 +241,24 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
expectedStatus: http.StatusUnprocessableEntity,
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
expectedBody: false,
|
expectedBody: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "POST UnprocessableEntity when both peer and peer_groups are provided",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/routes",
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer\":\"%s\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "POST UnprocessableEntity when no peer and peer_groups are provided",
|
||||||
|
requestType: http.MethodPost,
|
||||||
|
requestPath: "/api/routes",
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "PUT OK",
|
name: "PUT OK",
|
||||||
requestType: http.MethodPut,
|
requestType: http.MethodPut,
|
||||||
@@ -249,7 +271,27 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
Description: "Post",
|
Description: "Post",
|
||||||
NetworkId: "awesomeNet",
|
NetworkId: "awesomeNet",
|
||||||
Network: "192.168.0.0/16",
|
Network: "192.168.0.0/16",
|
||||||
Peer: existingPeerID,
|
Peer: &existingPeerID,
|
||||||
|
NetworkType: route.IPv4NetworkString,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: false,
|
||||||
|
Groups: []string{existingGroupID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT OK when peer_groups provided",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/routes/" + existingRouteID,
|
||||||
|
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingGroupID, existingGroupID)),
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
expectedBody: true,
|
||||||
|
expectedRoute: &api.Route{
|
||||||
|
Id: existingRouteID,
|
||||||
|
Description: "Post",
|
||||||
|
NetworkId: "awesomeNet",
|
||||||
|
Network: "192.168.0.0/16",
|
||||||
|
Peer: &emptyString,
|
||||||
|
PeerGroups: &[]string{existingGroupID},
|
||||||
NetworkType: route.IPv4NetworkString,
|
NetworkType: route.IPv4NetworkString,
|
||||||
Masquerade: false,
|
Masquerade: false,
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -272,6 +314,14 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
expectedStatus: http.StatusUnprocessableEntity,
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
expectedBody: false,
|
expectedBody: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "PUT Non Linux Peer",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/routes/" + existingRouteID,
|
||||||
|
requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "PUT Invalid Network Identifier",
|
name: "PUT Invalid Network Identifier",
|
||||||
requestType: http.MethodPut,
|
requestType: http.MethodPut,
|
||||||
@@ -288,6 +338,24 @@ func TestRoutesHandlers(t *testing.T) {
|
|||||||
expectedStatus: http.StatusUnprocessableEntity,
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
expectedBody: false,
|
expectedBody: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "PUT UnprocessableEntity when both peer and peer_groups are provided",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/routes/" + existingRouteID,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer\":\"%s\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PUT UnprocessableEntity when no peer and peer_groups are provided",
|
||||||
|
requestType: http.MethodPut,
|
||||||
|
requestPath: "/api/routes/" + existingRouteID,
|
||||||
|
requestBody: bytes.NewBuffer(
|
||||||
|
[]byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))),
|
||||||
|
expectedStatus: http.StatusUnprocessableEntity,
|
||||||
|
expectedBody: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
p := initRoutesTestData()
|
p := initRoutesTestData()
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ func WriteErrorResponse(errMsg string, httpStatus int, w http.ResponseWriter) {
|
|||||||
// WriteError converts an error to an JSON error response.
|
// WriteError converts an error to an JSON error response.
|
||||||
// If it is known internal error of type server.Error then it sets the messages from the error, a generic message otherwise
|
// If it is known internal error of type server.Error then it sets the messages from the error, a generic message otherwise
|
||||||
func WriteError(err error, w http.ResponseWriter) {
|
func WriteError(err error, w http.ResponseWriter) {
|
||||||
|
log.Errorf("got a handler error: %s", err.Error())
|
||||||
errStatus, ok := status.FromError(err)
|
errStatus, ok := status.FromError(err)
|
||||||
httpStatus := http.StatusInternalServerError
|
httpStatus := http.StatusInternalServerError
|
||||||
msg := "internal server error"
|
msg := "internal server error"
|
||||||
|
|||||||
@@ -513,7 +513,9 @@ func buildUserExportRequest() (string, error) {
|
|||||||
return string(str), nil
|
return string(str), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*http.Request, error) {
|
func (am *Auth0Manager) createRequest(
|
||||||
|
method string, endpoint string, body io.Reader,
|
||||||
|
) (*http.Request, error) {
|
||||||
jwtToken, err := am.credentials.Authenticate()
|
jwtToken, err := am.credentials.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -521,17 +523,23 @@ func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*
|
|||||||
|
|
||||||
reqURL := am.authIssuer + endpoint
|
reqURL := am.authIssuer + endpoint
|
||||||
|
|
||||||
payload := strings.NewReader(payloadStr)
|
req, err := http.NewRequest(method, reqURL, body)
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", reqURL, payload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *Auth0Manager) createPostRequest(endpoint string, payloadStr string) (*http.Request, error) {
|
||||||
|
req, err := am.createRequest("POST", endpoint, strings.NewReader(payloadStr))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
req.Header.Add("content-type", "application/json")
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
@@ -737,6 +745,38 @@ func (am *Auth0Manager) InviteUserByID(userID string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser from Auth0
|
||||||
|
func (am *Auth0Manager) DeleteUser(userID string) error {
|
||||||
|
req, err := am.createRequest(http.MethodDelete, "/api/v2/users/"+url.QueryEscape(userID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := am.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("execute delete request: %v", err)
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountRequestError()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("close delete request body: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if resp.StatusCode != 204 {
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
|
// checkExportJobStatus checks the status of the job created at CreateExportUsersJob.
|
||||||
// If the status is "completed", then return the downloadLink
|
// If the status is "completed", then return the downloadLink
|
||||||
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {
|
func (am *Auth0Manager) checkExportJobStatus(jobID string) (bool, string, error) {
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthentikManager authentik manager client instance.
|
// AuthentikManager authentik manager client instance.
|
||||||
@@ -209,47 +210,7 @@ func (ac *AuthentikCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
func (am *AuthentikManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
func (am *AuthentikManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
ctx, err := am.authenticationContext()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
userPk, err := strconv.ParseInt(userID, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pendingInvite bool
|
|
||||||
if appMetadata.WTPendingInvite != nil {
|
|
||||||
pendingInvite = *appMetadata.WTPendingInvite
|
|
||||||
}
|
|
||||||
|
|
||||||
patchedUserReq := api.PatchedUserRequest{
|
|
||||||
Attributes: map[string]interface{}{
|
|
||||||
wtAccountID: appMetadata.WTAccountID,
|
|
||||||
wtPendingInvite: pendingInvite,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, resp, err := am.apiClient.CoreApi.CoreUsersPartialUpdate(ctx, int32(userPk)).
|
|
||||||
PatchedUserRequest(patchedUserReq).
|
|
||||||
Execute()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to update user %s, statusCode %d", userID, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,41 +243,26 @@ func (am *AuthentikManager) GetUserDataByID(userID string, appMetadata AppMetada
|
|||||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseAuthentikUser(*user)
|
userData := parseAuthentikUser(*user)
|
||||||
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
|
return userData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
ctx, err := am.authenticationContext()
|
users, err := am.getAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountFilter := fmt.Sprintf("{%q:%q}", wtAccountID, accountID)
|
|
||||||
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Attributes(accountFilter).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
if am.appMetrics != nil {
|
||||||
am.appMetrics.IDPMetrics().CountGetAccount()
|
am.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
for index, user := range users {
|
||||||
if am.appMetrics != nil {
|
user.AppMetadata.WTAccountID = accountID
|
||||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
users[index] = user
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
|
||||||
for _, user := range userList.Results {
|
|
||||||
userData, err := parseAuthentikUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
users = append(users, userData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
@@ -325,89 +271,62 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {
|
func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
ctx, err := am.authenticationContext()
|
users, err := am.getAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Execute()
|
indexedUsers := make(map[string][]*UserData)
|
||||||
if err != nil {
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
if am.appMetrics != nil {
|
||||||
am.appMetrics.IDPMetrics().CountGetAllAccounts()
|
am.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
return indexedUsers, nil
|
||||||
if am.appMetrics != nil {
|
}
|
||||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
// getAllUsers returns all users in a Authentik account.
|
||||||
for _, user := range userList.Results {
|
func (am *AuthentikManager) getAllUsers() ([]*UserData, error) {
|
||||||
userData, err := parseAuthentikUser(user)
|
users := make([]*UserData, 0)
|
||||||
|
|
||||||
|
page := int32(1)
|
||||||
|
for {
|
||||||
|
ctx, err := am.authenticationContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Page(page).Execute()
|
||||||
if accountID != "" {
|
if err != nil {
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
return nil, err
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
||||||
}
|
}
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range userList.Results {
|
||||||
|
users = append(users, parseAuthentikUser(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
page = int32(userList.GetPagination().Next)
|
||||||
|
if userList.GetPagination().Next == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedUsers, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in authentik Idp and sends an invitation.
|
// CreateUser creates a new user in authentik Idp and sends an invitation.
|
||||||
func (am *AuthentikManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (am *AuthentikManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
ctx, err := am.authenticationContext()
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupID, err := am.getUserGroupByName("netbird")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultBoolValue := true
|
|
||||||
createUserRequest := api.UserRequest{
|
|
||||||
Email: &email,
|
|
||||||
Name: name,
|
|
||||||
IsActive: &defaultBoolValue,
|
|
||||||
Groups: []string{groupID},
|
|
||||||
Username: email,
|
|
||||||
Attributes: map[string]interface{}{
|
|
||||||
wtAccountID: accountID,
|
|
||||||
wtPendingInvite: &defaultBoolValue,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
user, resp, err := am.apiClient.CoreApi.CoreUsersCreate(ctx).UserRequest(createUserRequest).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseAuthentikUser(*user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
@@ -437,11 +356,7 @@ func (am *AuthentikManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0)
|
||||||
for _, user := range userList.Results {
|
for _, user := range userList.Results {
|
||||||
userData, err := parseAuthentikUser(user)
|
users = append(users, parseAuthentikUser(user))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
users = append(users, userData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
@@ -453,6 +368,38 @@ func (am *AuthentikManager) InviteUserByID(_ string) error {
|
|||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser from Authentik
|
||||||
|
func (am *AuthentikManager) DeleteUser(userID string) error {
|
||||||
|
ctx, err := am.authenticationContext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userPk, err := strconv.ParseInt(userID, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := am.apiClient.CoreApi.CoreUsersDestroy(ctx, int32(userPk)).Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint
|
||||||
|
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to delete user %s, statusCode %d", userID, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (am *AuthentikManager) authenticationContext() (context.Context, error) {
|
func (am *AuthentikManager) authenticationContext() (context.Context, error) {
|
||||||
jwtToken, err := am.credentials.Authenticate()
|
jwtToken, err := am.credentials.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -468,64 +415,10 @@ func (am *AuthentikManager) authenticationContext() (context.Context, error) {
|
|||||||
return context.WithValue(context.Background(), api.ContextAPIKeys, value), nil
|
return context.WithValue(context.Background(), api.ContextAPIKeys, value), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserGroupByName retrieves the user group for assigning new users.
|
func parseAuthentikUser(user api.User) *UserData {
|
||||||
// If the group is not found, a new group with the specified name will be created.
|
|
||||||
func (am *AuthentikManager) getUserGroupByName(name string) (string, error) {
|
|
||||||
ctx, err := am.authenticationContext()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupList, resp, err := am.apiClient.CoreApi.CoreGroupsList(ctx).Name(name).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if groupList != nil {
|
|
||||||
if len(groupList.Results) > 0 {
|
|
||||||
return groupList.Results[0].Pk, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createGroupRequest := api.GroupRequest{Name: name}
|
|
||||||
group, resp, err := am.apiClient.CoreApi.CoreGroupsCreate(ctx).GroupRequest(createGroupRequest).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return "", fmt.Errorf("unable to create user group, statusCode: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return group.Pk, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAuthentikUser(user api.User) (*UserData, error) {
|
|
||||||
var attributes struct {
|
|
||||||
AccountID string `json:"wt_account_id"`
|
|
||||||
PendingInvite bool `json:"wt_pending_invite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
helper := JsonParser{}
|
|
||||||
buf, err := helper.Marshal(user.Attributes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = helper.Unmarshal(buf, &attributes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserData{
|
return &UserData{
|
||||||
Email: *user.Email,
|
Email: *user.Email,
|
||||||
Name: user.Name,
|
Name: user.Name,
|
||||||
ID: strconv.FormatInt(int64(user.Pk), 10),
|
ID: strconv.FormatInt(int64(user.Pk), 10),
|
||||||
AppMetadata: AppMetadata{
|
}
|
||||||
WTAccountID: attributes.AccountID,
|
|
||||||
WTPendingInvite: &attributes.PendingInvite,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -11,18 +10,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const profileFields = "id,displayName,mail,userPrincipalName"
|
||||||
// azure extension properties template
|
|
||||||
wtAccountIDTpl = "extension_%s_wt_account_id"
|
|
||||||
wtPendingInviteTpl = "extension_%s_wt_pending_invite"
|
|
||||||
|
|
||||||
profileFields = "id,displayName,mail,userPrincipalName"
|
|
||||||
extensionFields = "id,name,targetObjects"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AzureManager azure manager client instance.
|
// AzureManager azure manager client instance.
|
||||||
type AzureManager struct {
|
type AzureManager struct {
|
||||||
@@ -58,21 +51,6 @@ type AzureCredentials struct {
|
|||||||
// azureProfile represents an azure user profile.
|
// azureProfile represents an azure user profile.
|
||||||
type azureProfile map[string]any
|
type azureProfile map[string]any
|
||||||
|
|
||||||
// passwordProfile represent authentication method for,
|
|
||||||
// newly created user profile.
|
|
||||||
type passwordProfile struct {
|
|
||||||
ForceChangePasswordNextSignIn bool `json:"forceChangePasswordNextSignIn"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// azureExtension represent custom attribute,
|
|
||||||
// that can be added to user objects in Azure Active Directory (AD).
|
|
||||||
type azureExtension struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
DataType string `json:"dataType"`
|
|
||||||
TargetObjects []string `json:"targetObjects"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAzureManager creates a new instance of the AzureManager.
|
// NewAzureManager creates a new instance of the AzureManager.
|
||||||
func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics) (*AzureManager, error) {
|
func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics) (*AzureManager, error) {
|
||||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
@@ -115,7 +93,7 @@ func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics)
|
|||||||
appMetrics: appMetrics,
|
appMetrics: appMetrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
manager := &AzureManager{
|
return &AzureManager{
|
||||||
ObjectID: config.ObjectID,
|
ObjectID: config.ObjectID,
|
||||||
ClientID: config.ClientID,
|
ClientID: config.ClientID,
|
||||||
GraphAPIEndpoint: config.GraphAPIEndpoint,
|
GraphAPIEndpoint: config.GraphAPIEndpoint,
|
||||||
@@ -123,14 +101,7 @@ func NewAzureManager(config AzureClientConfig, appMetrics telemetry.AppMetrics)
|
|||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
helper: helper,
|
helper: helper,
|
||||||
appMetrics: appMetrics,
|
appMetrics: appMetrics,
|
||||||
}
|
}, nil
|
||||||
|
|
||||||
err := manager.configureAppMetadata()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// jwtStillValid returns true if the token still valid and have enough time to be used and get a response from azure.
|
// jwtStillValid returns true if the token still valid and have enough time to be used and get a response from azure.
|
||||||
@@ -236,44 +207,14 @@ func (ac *AzureCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in azure AD Idp.
|
// CreateUser creates a new user in azure AD Idp.
|
||||||
func (am *AzureManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (am *AzureManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
payload, err := buildAzureCreateUserRequestPayload(email, name, accountID, am.ClientID)
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := am.post("users", payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
var profile azureProfile
|
|
||||||
err = am.helper.Unmarshal(body, &profile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
|
||||||
profile[wtAccountIDField] = accountID
|
|
||||||
|
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
profile[wtPendingInviteField] = true
|
|
||||||
|
|
||||||
return profile.userData(am.ClientID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserDataByID requests user data from keycloak via ID.
|
// GetUserDataByID requests user data from keycloak via ID.
|
||||||
func (am *AzureManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
func (am *AzureManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
|
||||||
|
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Add("$select", selectFields)
|
q.Add("$select", profileFields)
|
||||||
|
|
||||||
body, err := am.get("users/"+userID, q)
|
body, err := am.get("users/"+userID, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -290,18 +231,17 @@ func (am *AzureManager) GetUserDataByID(userID string, appMetadata AppMetadata)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return profile.userData(am.ClientID), nil
|
userData := profile.userData()
|
||||||
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
|
return userData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
// If no users have been found, this function returns an empty list.
|
// If no users have been found, this function returns an empty list.
|
||||||
func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) {
|
func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
|
||||||
|
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Add("$select", selectFields)
|
q.Add("$select", profileFields)
|
||||||
|
|
||||||
body, err := am.get("users/"+email, q)
|
body, err := am.get("users/"+email, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -319,22 +259,14 @@ func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0)
|
||||||
users = append(users, profile.userData(am.ClientID))
|
users = append(users, profile.userData())
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
users, err := am.getAllUsers()
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
|
||||||
|
|
||||||
q := url.Values{}
|
|
||||||
q.Add("$select", selectFields)
|
|
||||||
q.Add("$filter", fmt.Sprintf("%s eq '%s'", wtAccountIDField, accountID))
|
|
||||||
|
|
||||||
body, err := am.get("users", q)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -343,15 +275,9 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
am.appMetrics.IDPMetrics().CountGetAccount()
|
am.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
var profiles struct{ Value []azureProfile }
|
for index, user := range users {
|
||||||
err = am.helper.Unmarshal(body, &profiles)
|
user.AppMetadata.WTAccountID = accountID
|
||||||
if err != nil {
|
users[index] = user
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
|
||||||
for _, profile := range profiles.Value {
|
|
||||||
users = append(users, profile.userData(am.ClientID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
@@ -360,73 +286,48 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (am *AzureManager) GetAllAccounts() (map[string][]*UserData, error) {
|
func (am *AzureManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
users, err := am.getAllUsers()
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
selectFields := strings.Join([]string{profileFields, wtAccountIDField, wtPendingInviteField}, ",")
|
|
||||||
|
|
||||||
q := url.Values{}
|
|
||||||
q.Add("$select", selectFields)
|
|
||||||
|
|
||||||
body, err := am.get("users", q)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountGetAllAccounts()
|
|
||||||
}
|
|
||||||
|
|
||||||
var profiles struct{ Value []azureProfile }
|
|
||||||
err = am.helper.Unmarshal(body, &profiles)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
indexedUsers := make(map[string][]*UserData)
|
||||||
for _, profile := range profiles.Value {
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...)
|
||||||
userData := profile.userData(am.ClientID)
|
|
||||||
|
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
|
||||||
if accountID != "" {
|
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if am.appMetrics != nil {
|
||||||
|
am.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedUsers, nil
|
return indexedUsers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID.
|
// UpdateUserAppMetadata updates user app metadata based on userID.
|
||||||
func (am *AzureManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
func (am *AzureManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteUserByID resend invitations to users who haven't activated,
|
||||||
|
// their accounts prior to the expiration period.
|
||||||
|
func (am *AzureManager) InviteUserByID(_ string) error {
|
||||||
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser from Azure.
|
||||||
|
func (am *AzureManager) DeleteUser(userID string) error {
|
||||||
jwtToken, err := am.credentials.Authenticate()
|
jwtToken, err := am.credentials.Authenticate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
reqURL := fmt.Sprintf("%s/users/%s", am.GraphAPIEndpoint, url.QueryEscape(userID))
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||||
|
|
||||||
data, err := am.helper.Marshal(map[string]any{
|
|
||||||
wtAccountIDField: appMetadata.WTAccountID,
|
|
||||||
wtPendingInviteField: appMetadata.WTPendingInvite,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
payload := strings.NewReader(string(data))
|
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/users/%s", am.GraphAPIEndpoint, userID)
|
|
||||||
req, err := http.NewRequest(http.MethodPatch, reqURL, payload)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||||
req.Header.Add("content-type", "application/json")
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
log.Debugf("updating idp metadata for user %s", userID)
|
log.Debugf("delete idp user %s", userID)
|
||||||
|
|
||||||
resp, err := am.httpClient.Do(req)
|
resp, err := am.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -438,95 +339,47 @@ func (am *AzureManager) UpdateUserAppMetadata(userID string, appMetadata AppMeta
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if am.appMetrics != nil {
|
if am.appMetrics != nil {
|
||||||
am.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
am.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
return fmt.Errorf("unable to update the appMetadata, statusCode %d", resp.StatusCode)
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteUserByID resend invitations to users who haven't activated,
|
// getAllUsers returns all users in an Azure AD account.
|
||||||
// their accounts prior to the expiration period.
|
func (am *AzureManager) getAllUsers() ([]*UserData, error) {
|
||||||
func (am *AzureManager) InviteUserByID(_ string) error {
|
users := make([]*UserData, 0)
|
||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AzureManager) getUserExtensions() ([]azureExtension, error) {
|
|
||||||
q := url.Values{}
|
q := url.Values{}
|
||||||
q.Add("$select", extensionFields)
|
q.Add("$select", profileFields)
|
||||||
|
q.Add("$top", "500")
|
||||||
|
|
||||||
resource := fmt.Sprintf("applications/%s/extensionProperties", am.ObjectID)
|
for nextLink := "users"; nextLink != ""; {
|
||||||
body, err := am.get(resource, q)
|
body, err := am.get(nextLink, q)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var extensions struct{ Value []azureExtension }
|
|
||||||
err = am.helper.Unmarshal(body, &extensions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (am *AzureManager) createUserExtension(name string) (*azureExtension, error) {
|
|
||||||
extension := azureExtension{
|
|
||||||
Name: name,
|
|
||||||
DataType: "string",
|
|
||||||
TargetObjects: []string{"User"},
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := am.helper.Marshal(extension)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resource := fmt.Sprintf("applications/%s/extensionProperties", am.ObjectID)
|
|
||||||
body, err := am.post(resource, string(payload))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var userExtension azureExtension
|
|
||||||
err = am.helper.Unmarshal(body, &userExtension)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &userExtension, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// configureAppMetadata sets up app metadata extensions if they do not exists.
|
|
||||||
func (am *AzureManager) configureAppMetadata() error {
|
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, am.ClientID)
|
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, am.ClientID)
|
|
||||||
|
|
||||||
extensions, err := am.getUserExtensions()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the wt_account_id extension does not already exist, create it.
|
|
||||||
if !hasExtension(extensions, wtAccountIDField) {
|
|
||||||
_, err = am.createUserExtension(wtAccountID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the wt_pending_invite extension does not already exist, create it.
|
var profiles struct {
|
||||||
if !hasExtension(extensions, wtPendingInviteField) {
|
Value []azureProfile
|
||||||
_, err = am.createUserExtension(wtPendingInvite)
|
NextLink string `json:"@odata.nextLink"`
|
||||||
|
}
|
||||||
|
err = am.helper.Unmarshal(body, &profiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, profile := range profiles.Value {
|
||||||
|
users = append(users, profile.userData())
|
||||||
|
}
|
||||||
|
|
||||||
|
nextLink = profiles.NextLink
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// get perform Get requests.
|
// get perform Get requests.
|
||||||
@@ -536,7 +389,14 @@ func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/%s?%s", am.GraphAPIEndpoint, resource, q.Encode())
|
var reqURL string
|
||||||
|
if strings.HasPrefix(resource, "https") {
|
||||||
|
// Already an absolute URL for paging
|
||||||
|
reqURL = resource
|
||||||
|
} else {
|
||||||
|
reqURL = fmt.Sprintf("%s/%s?%s", am.GraphAPIEndpoint, resource, q.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -565,44 +425,8 @@ func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) {
|
|||||||
return io.ReadAll(resp.Body)
|
return io.ReadAll(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// post perform Post requests.
|
|
||||||
func (am *AzureManager) post(resource string, body string) ([]byte, error) {
|
|
||||||
jwtToken, err := am.credentials.Authenticate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/%s", am.GraphAPIEndpoint, resource)
|
|
||||||
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
|
||||||
req.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
resp, err := am.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountRequestError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
if am.appMetrics != nil {
|
|
||||||
am.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unable to post %s, statusCode %d", reqURL, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return io.ReadAll(resp.Body)
|
|
||||||
}
|
|
||||||
|
|
||||||
// userData construct user data from keycloak profile.
|
// userData construct user data from keycloak profile.
|
||||||
func (ap azureProfile) userData(clientID string) *UserData {
|
func (ap azureProfile) userData() *UserData {
|
||||||
id, ok := ap["id"].(string)
|
id, ok := ap["id"].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
id = ""
|
id = ""
|
||||||
@@ -618,66 +442,9 @@ func (ap azureProfile) userData(clientID string) *UserData {
|
|||||||
name = ""
|
name = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
accountIDField := extensionName(wtAccountIDTpl, clientID)
|
|
||||||
accountID, ok := ap[accountIDField].(string)
|
|
||||||
if !ok {
|
|
||||||
accountID = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingInviteField := extensionName(wtPendingInviteTpl, clientID)
|
|
||||||
pendingInvite, ok := ap[pendingInviteField].(bool)
|
|
||||||
if !ok {
|
|
||||||
pendingInvite = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserData{
|
return &UserData{
|
||||||
Email: email,
|
Email: email,
|
||||||
Name: name,
|
Name: name,
|
||||||
ID: id,
|
ID: id,
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: accountID,
|
|
||||||
WTPendingInvite: &pendingInvite,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAzureCreateUserRequestPayload(email, name, accountID, clientID string) (string, error) {
|
|
||||||
wtAccountIDField := extensionName(wtAccountIDTpl, clientID)
|
|
||||||
wtPendingInviteField := extensionName(wtPendingInviteTpl, clientID)
|
|
||||||
|
|
||||||
req := &azureProfile{
|
|
||||||
"accountEnabled": true,
|
|
||||||
"displayName": name,
|
|
||||||
"mailNickName": strings.Join(strings.Split(name, " "), ""),
|
|
||||||
"userPrincipalName": email,
|
|
||||||
"passwordProfile": passwordProfile{
|
|
||||||
ForceChangePasswordNextSignIn: true,
|
|
||||||
Password: GeneratePassword(8, 1, 1, 1),
|
|
||||||
},
|
|
||||||
wtAccountIDField: accountID,
|
|
||||||
wtPendingInviteField: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(str), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extensionName(extensionTpl, clientID string) string {
|
|
||||||
clientID = strings.ReplaceAll(clientID, "-", "")
|
|
||||||
return fmt.Sprintf(extensionTpl, clientID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasExtension checks whether a given extension by name,
|
|
||||||
// exists in an list of extensions.
|
|
||||||
func hasExtension(extensions []azureExtension, name string) bool {
|
|
||||||
for _, ext := range extensions {
|
|
||||||
if ext.Name == name {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,15 +8,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockAzureCredentials struct {
|
|
||||||
jwtToken JWTToken
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *mockAzureCredentials) Authenticate() (JWTToken, error) {
|
|
||||||
return mc.jwtToken, mc.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAzureJwtStillValid(t *testing.T) {
|
func TestAzureJwtStillValid(t *testing.T) {
|
||||||
type jwtStillValidTest struct {
|
type jwtStillValidTest struct {
|
||||||
name string
|
name string
|
||||||
@@ -124,206 +115,63 @@ func TestAzureAuthenticate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAzureUpdateUserAppMetadata(t *testing.T) {
|
|
||||||
type updateUserAppMetadataTest struct {
|
|
||||||
name string
|
|
||||||
inputReqBody string
|
|
||||||
expectedReqBody string
|
|
||||||
appMetadata AppMetadata
|
|
||||||
statusCode int
|
|
||||||
helper ManagerHelper
|
|
||||||
managerCreds ManagerCredentials
|
|
||||||
assertErrFunc assert.ErrorAssertionFunc
|
|
||||||
assertErrFuncMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Authentication",
|
|
||||||
expectedReqBody: "",
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 400,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockAzureCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
err: fmt.Errorf("error"),
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Status Code",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":null}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 400,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockAzureCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Response Parsing",
|
|
||||||
statusCode: 400,
|
|
||||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
|
||||||
managerCreds: &mockAzureCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
|
||||||
name: "Good request",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":null}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 204,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockAzureCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
invite := true
|
|
||||||
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
|
||||||
name: "Update Pending Invite",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"extension__wt_account_id\":\"%s\",\"extension__wt_pending_invite\":true}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: AppMetadata{
|
|
||||||
WTAccountID: "ok",
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
},
|
|
||||||
statusCode: 204,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockAzureCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
|
||||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4, updateUserAppMetadataTestCase5} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
reqClient := mockHTTPClient{
|
|
||||||
resBody: testCase.inputReqBody,
|
|
||||||
code: testCase.statusCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := &AzureManager{
|
|
||||||
httpClient: &reqClient,
|
|
||||||
credentials: testCase.managerCreds,
|
|
||||||
helper: testCase.helper,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
|
||||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAzureProfile(t *testing.T) {
|
func TestAzureProfile(t *testing.T) {
|
||||||
type azureProfileTest struct {
|
type azureProfileTest struct {
|
||||||
name string
|
name string
|
||||||
clientID string
|
|
||||||
invite bool
|
invite bool
|
||||||
inputProfile azureProfile
|
inputProfile azureProfile
|
||||||
expectedUserData UserData
|
expectedUserData UserData
|
||||||
}
|
}
|
||||||
|
|
||||||
azureProfileTestCase1 := azureProfileTest{
|
azureProfileTestCase1 := azureProfileTest{
|
||||||
name: "Good Request",
|
name: "Good Request",
|
||||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
invite: false,
|
||||||
invite: false,
|
|
||||||
inputProfile: azureProfile{
|
inputProfile: azureProfile{
|
||||||
"id": "test1",
|
"id": "test1",
|
||||||
"displayName": "John Doe",
|
"displayName": "John Doe",
|
||||||
"userPrincipalName": "test1@test.com",
|
"userPrincipalName": "test1@test.com",
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": false,
|
|
||||||
},
|
},
|
||||||
expectedUserData: UserData{
|
expectedUserData: UserData{
|
||||||
Email: "test1@test.com",
|
Email: "test1@test.com",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
ID: "test1",
|
ID: "test1",
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: "1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
azureProfileTestCase2 := azureProfileTest{
|
azureProfileTestCase2 := azureProfileTest{
|
||||||
name: "Missing User ID",
|
name: "Missing User ID",
|
||||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
invite: true,
|
||||||
invite: true,
|
|
||||||
inputProfile: azureProfile{
|
inputProfile: azureProfile{
|
||||||
"displayName": "John Doe",
|
"displayName": "John Doe",
|
||||||
"userPrincipalName": "test2@test.com",
|
"userPrincipalName": "test2@test.com",
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": true,
|
|
||||||
},
|
},
|
||||||
expectedUserData: UserData{
|
expectedUserData: UserData{
|
||||||
Email: "test2@test.com",
|
Email: "test2@test.com",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: "1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
azureProfileTestCase3 := azureProfileTest{
|
azureProfileTestCase3 := azureProfileTest{
|
||||||
name: "Missing User Name",
|
name: "Missing User Name",
|
||||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
invite: false,
|
||||||
invite: false,
|
|
||||||
inputProfile: azureProfile{
|
inputProfile: azureProfile{
|
||||||
"id": "test3",
|
"id": "test3",
|
||||||
"userPrincipalName": "test3@test.com",
|
"userPrincipalName": "test3@test.com",
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_account_id": "1",
|
|
||||||
"extension_25d0b095048440d29fd303f8f4abbb3c_wt_pending_invite": false,
|
|
||||||
},
|
},
|
||||||
expectedUserData: UserData{
|
expectedUserData: UserData{
|
||||||
ID: "test3",
|
ID: "test3",
|
||||||
Email: "test3@test.com",
|
Email: "test3@test.com",
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: "1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
azureProfileTestCase4 := azureProfileTest{
|
for _, testCase := range []azureProfileTest{azureProfileTestCase1, azureProfileTestCase2, azureProfileTestCase3} {
|
||||||
name: "Missing Extension Fields",
|
|
||||||
clientID: "25d0b095-0484-40d2-9fd3-03f8f4abbb3c",
|
|
||||||
invite: false,
|
|
||||||
inputProfile: azureProfile{
|
|
||||||
"id": "test4",
|
|
||||||
"displayName": "John Doe",
|
|
||||||
"userPrincipalName": "test4@test.com",
|
|
||||||
},
|
|
||||||
expectedUserData: UserData{
|
|
||||||
ID: "test4",
|
|
||||||
Name: "John Doe",
|
|
||||||
Email: "test4@test.com",
|
|
||||||
AppMetadata: AppMetadata{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []azureProfileTest{azureProfileTestCase1, azureProfileTestCase2, azureProfileTestCase3, azureProfileTestCase4} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
|
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
|
||||||
userData := testCase.inputProfile.userData(testCase.clientID)
|
userData := testCase.inputProfile.userData()
|
||||||
|
|
||||||
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTAccountID, userData.AppMetadata.WTAccountID, "Account id should match")
|
|
||||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTPendingInvite, userData.AppMetadata.WTPendingInvite, "Pending invite should match")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
admin "google.golang.org/api/admin/directory/v1"
|
admin "google.golang.org/api/admin/directory/v1"
|
||||||
"google.golang.org/api/googleapi"
|
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GoogleWorkspaceManager Google Workspace manager client instance.
|
// GoogleWorkspaceManager Google Workspace manager client instance.
|
||||||
@@ -73,17 +72,13 @@ func NewGoogleWorkspaceManager(config GoogleWorkspaceClientConfig, appMetrics te
|
|||||||
}
|
}
|
||||||
|
|
||||||
service, err := admin.NewService(context.Background(),
|
service, err := admin.NewService(context.Background(),
|
||||||
option.WithScopes(admin.AdminDirectoryUserScope, admin.AdminDirectoryUserschemaScope),
|
option.WithScopes(admin.AdminDirectoryUserReadonlyScope),
|
||||||
option.WithCredentials(adminCredentials),
|
option.WithCredentials(adminCredentials),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = configureAppMetadataSchema(service, config.CustomerID); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &GoogleWorkspaceManager{
|
return &GoogleWorkspaceManager{
|
||||||
usersService: service.Users,
|
usersService: service.Users,
|
||||||
CustomerID: config.CustomerID,
|
CustomerID: config.CustomerID,
|
||||||
@@ -95,33 +90,13 @@ func NewGoogleWorkspaceManager(config GoogleWorkspaceClientConfig, appMetrics te
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
metadata, err := gm.helper.Marshal(appMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &admin.User{
|
|
||||||
CustomSchemas: map[string]googleapi.RawMessage{
|
|
||||||
"app_metadata": metadata,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = gm.usersService.Update(userID, user).Do()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if gm.appMetrics != nil {
|
|
||||||
gm.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserDataByID requests user data from Google Workspace via ID.
|
// GetUserDataByID requests user data from Google Workspace via ID.
|
||||||
func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||||
user, err := gm.usersService.Get(userID).Projection("full").Do()
|
user, err := gm.usersService.Get(userID).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -130,105 +105,86 @@ func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata App
|
|||||||
gm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
gm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseGoogleWorkspaceUser(user)
|
userData := parseGoogleWorkspaceUser(user)
|
||||||
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
|
return userData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (gm *GoogleWorkspaceManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (gm *GoogleWorkspaceManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
query := fmt.Sprintf("app_metadata.wt_account_id=\"%s\"", accountID)
|
users, err := gm.getAllUsers()
|
||||||
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Query(query).Projection("full").Do()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
usersData := make([]*UserData, 0)
|
if gm.appMetrics != nil {
|
||||||
for _, user := range usersList.Users {
|
gm.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
userData, err := parseGoogleWorkspaceUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
usersData = append(usersData, userData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return usersData, nil
|
for index, user := range users {
|
||||||
|
user.AppMetadata.WTAccountID = accountID
|
||||||
|
users[index] = user
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, error) {
|
func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
usersList, err := gm.usersService.List().Customer(gm.CustomerID).Projection("full").Do()
|
users, err := gm.getAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indexedUsers := make(map[string][]*UserData)
|
||||||
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...)
|
||||||
|
|
||||||
if gm.appMetrics != nil {
|
if gm.appMetrics != nil {
|
||||||
gm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
gm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
return indexedUsers, nil
|
||||||
for _, user := range usersList.Users {
|
}
|
||||||
userData, err := parseGoogleWorkspaceUser(user)
|
|
||||||
|
// getAllUsers returns all users in a Google Workspace account filtered by customer ID.
|
||||||
|
func (gm *GoogleWorkspaceManager) getAllUsers() ([]*UserData, error) {
|
||||||
|
users := make([]*UserData, 0)
|
||||||
|
pageToken := ""
|
||||||
|
for {
|
||||||
|
call := gm.usersService.List().Customer(gm.CustomerID).MaxResults(500)
|
||||||
|
if pageToken != "" {
|
||||||
|
call.PageToken(pageToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := call.Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
for _, user := range resp.Users {
|
||||||
if accountID != "" {
|
users = append(users, parseGoogleWorkspaceUser(user))
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
}
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
pageToken = resp.NextPageToken
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
if pageToken == "" {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedUsers, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
// CreateUser creates a new user in Google Workspace and sends an invitation.
|
||||||
func (gm *GoogleWorkspaceManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (gm *GoogleWorkspaceManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
invite := true
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
metadata := AppMetadata{
|
|
||||||
WTAccountID: accountID,
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
}
|
|
||||||
|
|
||||||
username := &admin.UserName{}
|
|
||||||
fields := strings.Fields(name)
|
|
||||||
if n := len(fields); n > 0 {
|
|
||||||
username.GivenName = strings.Join(fields[:n-1], " ")
|
|
||||||
username.FamilyName = fields[n-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := gm.helper.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &admin.User{
|
|
||||||
Name: username,
|
|
||||||
PrimaryEmail: email,
|
|
||||||
CustomSchemas: map[string]googleapi.RawMessage{
|
|
||||||
"app_metadata": payload,
|
|
||||||
},
|
|
||||||
Password: GeneratePassword(8, 1, 1, 1),
|
|
||||||
}
|
|
||||||
user, err = gm.usersService.Insert(user).Do()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if gm.appMetrics != nil {
|
|
||||||
gm.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseGoogleWorkspaceUser(user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
// If no users have been found, this function returns an empty list.
|
// If no users have been found, this function returns an empty list.
|
||||||
func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, error) {
|
func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||||
user, err := gm.usersService.Get(email).Projection("full").Do()
|
user, err := gm.usersService.Get(email).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -237,13 +193,8 @@ func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, err
|
|||||||
gm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
gm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||||
}
|
}
|
||||||
|
|
||||||
userData, err := parseGoogleWorkspaceUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0)
|
||||||
users = append(users, userData)
|
users = append(users, parseGoogleWorkspaceUser(user))
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
@@ -254,6 +205,19 @@ func (gm *GoogleWorkspaceManager) InviteUserByID(_ string) error {
|
|||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUser from GoogleWorkspace.
|
||||||
|
func (gm *GoogleWorkspaceManager) DeleteUser(userID string) error {
|
||||||
|
if err := gm.usersService.Delete(userID).Do(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gm.appMetrics != nil {
|
||||||
|
gm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getGoogleCredentials retrieves Google credentials based on the provided serviceAccountKey.
|
// getGoogleCredentials retrieves Google credentials based on the provided serviceAccountKey.
|
||||||
// It decodes the base64-encoded serviceAccountKey and attempts to obtain credentials using it.
|
// It decodes the base64-encoded serviceAccountKey and attempts to obtain credentials using it.
|
||||||
// If that fails, it falls back to using the default Google credentials path.
|
// If that fails, it falls back to using the default Google credentials path.
|
||||||
@@ -268,8 +232,7 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
|||||||
creds, err := google.CredentialsFromJSON(
|
creds, err := google.CredentialsFromJSON(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
decodeKey,
|
decodeKey,
|
||||||
admin.AdminDirectoryUserschemaScope,
|
admin.AdminDirectoryUserReadonlyScope,
|
||||||
admin.AdminDirectoryUserScope,
|
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// No need to fallback to the default Google credentials path
|
// No need to fallback to the default Google credentials path
|
||||||
@@ -281,8 +244,7 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
|||||||
|
|
||||||
creds, err = google.FindDefaultCredentials(
|
creds, err = google.FindDefaultCredentials(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
admin.AdminDirectoryUserschemaScope,
|
admin.AdminDirectoryUserReadonlyScope,
|
||||||
admin.AdminDirectoryUserScope,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -291,62 +253,11 @@ func getGoogleCredentials(serviceAccountKey string) (*google.Credentials, error)
|
|||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureAppMetadataSchema create a custom schema for managing app metadata fields in Google Workspace.
|
|
||||||
func configureAppMetadataSchema(service *admin.Service, customerID string) error {
|
|
||||||
schemaList, err := service.Schemas.List(customerID).Do()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks if app_metadata schema is already created
|
|
||||||
for _, schema := range schemaList.Schemas {
|
|
||||||
if schema.SchemaName == "app_metadata" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new app_metadata schema
|
|
||||||
appMetadataSchema := &admin.Schema{
|
|
||||||
SchemaName: "app_metadata",
|
|
||||||
Fields: []*admin.SchemaFieldSpec{
|
|
||||||
{
|
|
||||||
FieldName: "wt_account_id",
|
|
||||||
FieldType: "STRING",
|
|
||||||
MultiValued: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
FieldName: "wt_pending_invite",
|
|
||||||
FieldType: "BOOL",
|
|
||||||
MultiValued: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
_, err = service.Schemas.Insert(customerID, appMetadataSchema).Do()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseGoogleWorkspaceUser parse google user to UserData.
|
// parseGoogleWorkspaceUser parse google user to UserData.
|
||||||
func parseGoogleWorkspaceUser(user *admin.User) (*UserData, error) {
|
func parseGoogleWorkspaceUser(user *admin.User) *UserData {
|
||||||
var appMetadata AppMetadata
|
|
||||||
|
|
||||||
// Get app metadata from custom schemas
|
|
||||||
if user.CustomSchemas != nil {
|
|
||||||
rawMessage := user.CustomSchemas["app_metadata"]
|
|
||||||
helper := JsonParser{}
|
|
||||||
|
|
||||||
if err := helper.Unmarshal(rawMessage, &appMetadata); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserData{
|
return &UserData{
|
||||||
ID: user.Id,
|
ID: user.Id,
|
||||||
Email: user.PrimaryEmail,
|
Email: user.PrimaryEmail,
|
||||||
Name: user.Name.FullName,
|
Name: user.Name.FullName,
|
||||||
AppMetadata: appMetadata,
|
}
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnsetAccountID is a special key to map users without an account ID
|
||||||
|
UnsetAccountID = "unset"
|
||||||
|
)
|
||||||
|
|
||||||
// Manager idp manager interface
|
// Manager idp manager interface
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
|
UpdateUserAppMetadata(userId string, appMetadata AppMetadata) error
|
||||||
@@ -18,6 +23,7 @@ type Manager interface {
|
|||||||
CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error)
|
CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error)
|
||||||
GetUserByEmail(email string) ([]*UserData, error)
|
GetUserByEmail(email string) ([]*UserData, error)
|
||||||
InviteUserByID(userID string) error
|
InviteUserByID(userID string) error
|
||||||
|
DeleteUser(userID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfig defines common client configuration for all IdP manager
|
// ClientConfig defines common client configuration for all IdP manager
|
||||||
@@ -37,10 +43,10 @@ type Config struct {
|
|||||||
ManagerType string
|
ManagerType string
|
||||||
ClientConfig *ClientConfig
|
ClientConfig *ClientConfig
|
||||||
ExtraConfig ExtraConfig
|
ExtraConfig ExtraConfig
|
||||||
Auth0ClientCredentials Auth0ClientConfig
|
Auth0ClientCredentials *Auth0ClientConfig
|
||||||
AzureClientCredentials AzureClientConfig
|
AzureClientCredentials *AzureClientConfig
|
||||||
KeycloakClientCredentials KeycloakClientConfig
|
KeycloakClientCredentials *KeycloakClientConfig
|
||||||
ZitadelClientCredentials ZitadelClientConfig
|
ZitadelClientCredentials *ZitadelClientConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManagerCredentials interface that authenticates using the credential of each type of idp
|
// ManagerCredentials interface that authenticates using the credential of each type of idp
|
||||||
@@ -96,7 +102,7 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
case "auth0":
|
case "auth0":
|
||||||
auth0ClientConfig := config.Auth0ClientCredentials
|
auth0ClientConfig := config.Auth0ClientCredentials
|
||||||
if config.ClientConfig != nil {
|
if config.ClientConfig != nil {
|
||||||
auth0ClientConfig = Auth0ClientConfig{
|
auth0ClientConfig = &Auth0ClientConfig{
|
||||||
Audience: config.ExtraConfig["Audience"],
|
Audience: config.ExtraConfig["Audience"],
|
||||||
AuthIssuer: config.ClientConfig.Issuer,
|
AuthIssuer: config.ClientConfig.Issuer,
|
||||||
ClientID: config.ClientConfig.ClientID,
|
ClientID: config.ClientConfig.ClientID,
|
||||||
@@ -105,11 +111,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewAuth0Manager(auth0ClientConfig, appMetrics)
|
return NewAuth0Manager(*auth0ClientConfig, appMetrics)
|
||||||
case "azure":
|
case "azure":
|
||||||
azureClientConfig := config.AzureClientCredentials
|
azureClientConfig := config.AzureClientCredentials
|
||||||
if config.ClientConfig != nil {
|
if config.ClientConfig != nil {
|
||||||
azureClientConfig = AzureClientConfig{
|
azureClientConfig = &AzureClientConfig{
|
||||||
ClientID: config.ClientConfig.ClientID,
|
ClientID: config.ClientConfig.ClientID,
|
||||||
ClientSecret: config.ClientConfig.ClientSecret,
|
ClientSecret: config.ClientConfig.ClientSecret,
|
||||||
GrantType: config.ClientConfig.GrantType,
|
GrantType: config.ClientConfig.GrantType,
|
||||||
@@ -119,11 +125,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewAzureManager(azureClientConfig, appMetrics)
|
return NewAzureManager(*azureClientConfig, appMetrics)
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
keycloakClientConfig := config.KeycloakClientCredentials
|
keycloakClientConfig := config.KeycloakClientCredentials
|
||||||
if config.ClientConfig != nil {
|
if config.ClientConfig != nil {
|
||||||
keycloakClientConfig = KeycloakClientConfig{
|
keycloakClientConfig = &KeycloakClientConfig{
|
||||||
ClientID: config.ClientConfig.ClientID,
|
ClientID: config.ClientConfig.ClientID,
|
||||||
ClientSecret: config.ClientConfig.ClientSecret,
|
ClientSecret: config.ClientConfig.ClientSecret,
|
||||||
GrantType: config.ClientConfig.GrantType,
|
GrantType: config.ClientConfig.GrantType,
|
||||||
@@ -132,11 +138,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewKeycloakManager(keycloakClientConfig, appMetrics)
|
return NewKeycloakManager(*keycloakClientConfig, appMetrics)
|
||||||
case "zitadel":
|
case "zitadel":
|
||||||
zitadelClientConfig := config.ZitadelClientCredentials
|
zitadelClientConfig := config.ZitadelClientCredentials
|
||||||
if config.ClientConfig != nil {
|
if config.ClientConfig != nil {
|
||||||
zitadelClientConfig = ZitadelClientConfig{
|
zitadelClientConfig = &ZitadelClientConfig{
|
||||||
ClientID: config.ClientConfig.ClientID,
|
ClientID: config.ClientConfig.ClientID,
|
||||||
ClientSecret: config.ClientConfig.ClientSecret,
|
ClientSecret: config.ClientConfig.ClientSecret,
|
||||||
GrantType: config.ClientConfig.GrantType,
|
GrantType: config.ClientConfig.GrantType,
|
||||||
@@ -145,7 +151,7 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewZitadelManager(zitadelClientConfig, appMetrics)
|
return NewZitadelManager(*zitadelClientConfig, appMetrics)
|
||||||
case "authentik":
|
case "authentik":
|
||||||
authentikConfig := AuthentikClientConfig{
|
authentikConfig := AuthentikClientConfig{
|
||||||
Issuer: config.ClientConfig.Issuer,
|
Issuer: config.ClientConfig.Issuer,
|
||||||
@@ -170,7 +176,11 @@ func NewManager(config Config, appMetrics telemetry.AppMetrics) (Manager, error)
|
|||||||
CustomerID: config.ExtraConfig["CustomerId"],
|
CustomerID: config.ExtraConfig["CustomerId"],
|
||||||
}
|
}
|
||||||
return NewGoogleWorkspaceManager(googleClientConfig, appMetrics)
|
return NewGoogleWorkspaceManager(googleClientConfig, appMetrics)
|
||||||
|
case "jumpcloud":
|
||||||
|
jumpcloudConfig := JumpCloudClientConfig{
|
||||||
|
APIToken: config.ExtraConfig["ApiToken"],
|
||||||
|
}
|
||||||
|
return NewJumpCloudManager(jumpcloudConfig, appMetrics)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
|
||||||
}
|
}
|
||||||
|
|||||||
257
management/server/idp/jumpcloud.go
Normal file
257
management/server/idp/jumpcloud.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package idp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/TheJumpCloud/jcapi-go/v1"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contentType = "application/json"
|
||||||
|
accept = "application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JumpCloudManager JumpCloud manager client instance.
|
||||||
|
type JumpCloudManager struct {
|
||||||
|
client *v1.APIClient
|
||||||
|
apiToken string
|
||||||
|
httpClient ManagerHTTPClient
|
||||||
|
credentials ManagerCredentials
|
||||||
|
helper ManagerHelper
|
||||||
|
appMetrics telemetry.AppMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// JumpCloudClientConfig JumpCloud manager client configurations.
|
||||||
|
type JumpCloudClientConfig struct {
|
||||||
|
APIToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// JumpCloudCredentials JumpCloud authentication information.
|
||||||
|
type JumpCloudCredentials struct {
|
||||||
|
clientConfig JumpCloudClientConfig
|
||||||
|
helper ManagerHelper
|
||||||
|
httpClient ManagerHTTPClient
|
||||||
|
appMetrics telemetry.AppMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJumpCloudManager creates a new instance of the JumpCloudManager.
|
||||||
|
func NewJumpCloudManager(config JumpCloudClientConfig, appMetrics telemetry.AppMetrics) (*JumpCloudManager, error) {
|
||||||
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
httpTransport.MaxIdleConns = 5
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
Transport: httpTransport,
|
||||||
|
}
|
||||||
|
helper := JsonParser{}
|
||||||
|
|
||||||
|
if config.APIToken == "" {
|
||||||
|
return nil, fmt.Errorf("jumpCloud IdP configuration is incomplete, ApiToken is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := v1.NewAPIClient(v1.NewConfiguration())
|
||||||
|
credentials := &JumpCloudCredentials{
|
||||||
|
clientConfig: config,
|
||||||
|
httpClient: httpClient,
|
||||||
|
helper: helper,
|
||||||
|
appMetrics: appMetrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &JumpCloudManager{
|
||||||
|
client: client,
|
||||||
|
apiToken: config.APIToken,
|
||||||
|
httpClient: httpClient,
|
||||||
|
credentials: credentials,
|
||||||
|
helper: helper,
|
||||||
|
appMetrics: appMetrics,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate retrieves access token to use the JumpCloud user API.
|
||||||
|
func (jc *JumpCloudCredentials) Authenticate() (JWTToken, error) {
|
||||||
|
return JWTToken{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JumpCloudManager) authenticationContext() context.Context {
|
||||||
|
return context.WithValue(context.Background(), v1.ContextAPIKey, v1.APIKey{
|
||||||
|
Key: jm.apiToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
|
func (jm *JumpCloudManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserDataByID requests user data from JumpCloud via ID.
|
||||||
|
func (jm *JumpCloudManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
||||||
|
authCtx := jm.authenticationContext()
|
||||||
|
user, resp, err := jm.client.SystemusersApi.SystemusersGet(authCtx, userID, contentType, accept, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountGetUserDataByID()
|
||||||
|
}
|
||||||
|
|
||||||
|
userData := parseJumpCloudUser(user)
|
||||||
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
|
return userData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccount returns all the users for a given profile.
|
||||||
|
func (jm *JumpCloudManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
|
authCtx := jm.authenticationContext()
|
||||||
|
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make([]*UserData, 0)
|
||||||
|
for _, user := range userList.Results {
|
||||||
|
userData := parseJumpCloudUser(user)
|
||||||
|
userData.AppMetadata.WTAccountID = accountID
|
||||||
|
|
||||||
|
users = append(users, userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
|
// It returns a list of users indexed by accountID.
|
||||||
|
func (jm *JumpCloudManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
|
authCtx := jm.authenticationContext()
|
||||||
|
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
indexedUsers := make(map[string][]*UserData)
|
||||||
|
for _, user := range userList.Results {
|
||||||
|
userData := parseJumpCloudUser(user)
|
||||||
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexedUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user in JumpCloud Idp and sends an invitation.
|
||||||
|
func (jm *JumpCloudManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail searches users with a given email.
|
||||||
|
// If no users have been found, this function returns an empty list.
|
||||||
|
func (jm *JumpCloudManager) GetUserByEmail(email string) ([]*UserData, error) {
|
||||||
|
searchFilter := map[string]interface{}{
|
||||||
|
"searchFilter": map[string]interface{}{
|
||||||
|
"filter": []string{email},
|
||||||
|
"fields": []string{"email"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
authCtx := jm.authenticationContext()
|
||||||
|
userList, resp, err := jm.client.SearchApi.SearchSystemusersPost(authCtx, contentType, accept, searchFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountGetUserByEmail()
|
||||||
|
}
|
||||||
|
|
||||||
|
usersData := make([]*UserData, 0)
|
||||||
|
for _, user := range userList.Results {
|
||||||
|
usersData = append(usersData, parseJumpCloudUser(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
return usersData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteUserByID resend invitations to users who haven't activated,
|
||||||
|
// their accounts prior to the expiration period.
|
||||||
|
func (jm *JumpCloudManager) InviteUserByID(_ string) error {
|
||||||
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser from jumpCloud directory
|
||||||
|
func (jm *JumpCloudManager) DeleteUser(userID string) error {
|
||||||
|
authCtx := jm.authenticationContext()
|
||||||
|
_, resp, err := jm.client.SystemusersApi.SystemusersDelete(authCtx, userID, contentType, accept, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if jm.appMetrics != nil {
|
||||||
|
jm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseJumpCloudUser parse JumpCloud system user returned from API V1 to UserData.
|
||||||
|
func parseJumpCloudUser(user v1.Systemuserreturn) *UserData {
|
||||||
|
names := []string{user.Firstname, user.Middlename, user.Lastname}
|
||||||
|
return &UserData{
|
||||||
|
Email: user.Email,
|
||||||
|
Name: strings.Join(names, " "),
|
||||||
|
ID: user.Id,
|
||||||
|
}
|
||||||
|
}
|
||||||
46
management/server/idp/jumpcloud_test.go
Normal file
46
management/server/idp/jumpcloud_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package idp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewJumpCloudManager(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
inputConfig JumpCloudClientConfig
|
||||||
|
assertErrFunc require.ErrorAssertionFunc
|
||||||
|
assertErrFuncMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultTestConfig := JumpCloudClientConfig{
|
||||||
|
APIToken: "test123",
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase1 := test{
|
||||||
|
name: "Good Configuration",
|
||||||
|
inputConfig: defaultTestConfig,
|
||||||
|
assertErrFunc: require.NoError,
|
||||||
|
assertErrFuncMessage: "shouldn't return error",
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase2Config := defaultTestConfig
|
||||||
|
testCase2Config.APIToken = ""
|
||||||
|
|
||||||
|
testCase2 := test{
|
||||||
|
name: "Missing APIToken Configuration",
|
||||||
|
inputConfig: testCase2Config,
|
||||||
|
assertErrFunc: require.Error,
|
||||||
|
assertErrFuncMessage: "should return error when field empty",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range []test{testCase1, testCase2} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
_, err := NewJumpCloudManager(testCase.inputConfig, &telemetry.MockAppMetrics{})
|
||||||
|
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,11 +16,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
wtAccountID = "wt_account_id"
|
|
||||||
wtPendingInvite = "wt_pending_invite"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeycloakManager keycloak manager client instance.
|
// KeycloakManager keycloak manager client instance.
|
||||||
type KeycloakManager struct {
|
type KeycloakManager struct {
|
||||||
adminEndpoint string
|
adminEndpoint string
|
||||||
@@ -51,28 +44,10 @@ type KeycloakCredentials struct {
|
|||||||
appMetrics telemetry.AppMetrics
|
appMetrics telemetry.AppMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// keycloakUserCredential describe the authentication method for,
|
|
||||||
// newly created user profile.
|
|
||||||
type keycloakUserCredential struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Temporary bool `json:"temporary"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// keycloakUserAttributes holds additional user data fields.
|
// keycloakUserAttributes holds additional user data fields.
|
||||||
type keycloakUserAttributes map[string][]string
|
type keycloakUserAttributes map[string][]string
|
||||||
|
|
||||||
// createUserRequest is a user create request.
|
// keycloakProfile represents a keycloak user profile response.
|
||||||
type keycloakCreateUserRequest struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
EmailVerified bool `json:"emailVerified"`
|
|
||||||
Credentials []keycloakUserCredential `json:"credentials"`
|
|
||||||
Attributes keycloakUserAttributes `json:"attributes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// keycloakProfile represents an keycloak user profile response.
|
|
||||||
type keycloakProfile struct {
|
type keycloakProfile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CreatedTimestamp int64 `json:"createdTimestamp"`
|
CreatedTimestamp int64 `json:"createdTimestamp"`
|
||||||
@@ -230,62 +205,8 @@ func (kc *KeycloakCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in keycloak Idp and sends an invite.
|
// CreateUser creates a new user in keycloak Idp and sends an invite.
|
||||||
func (km *KeycloakManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (km *KeycloakManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
jwtToken, err := km.credentials.Authenticate()
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
invite := true
|
|
||||||
appMetadata := AppMetadata{
|
|
||||||
WTAccountID: accountID,
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadString, err := buildKeycloakCreateUserRequestPayload(email, name, appMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/users", km.adminEndpoint)
|
|
||||||
payload := strings.NewReader(payloadString)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, reqURL, payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
|
||||||
req.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := km.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountRequestError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
locationHeader := resp.Header.Get("location")
|
|
||||||
userID, err := extractUserIDFromLocationHeader(locationHeader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return km.GetUserDataByID(userID, appMetadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
@@ -319,7 +240,7 @@ func (km *KeycloakManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUserDataByID requests user data from keycloak via ID.
|
// GetUserDataByID requests user data from keycloak via ID.
|
||||||
func (km *KeycloakManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) {
|
func (km *KeycloakManager) GetUserDataByID(userID string, _ AppMetadata) (*UserData, error) {
|
||||||
body, err := km.get("users/"+userID, nil)
|
body, err := km.get("users/"+userID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -338,12 +259,9 @@ func (km *KeycloakManager) GetUserDataByID(userID string, appMetadata AppMetadat
|
|||||||
return profile.userData(), nil
|
return profile.userData(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given account profile.
|
||||||
func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
q := url.Values{}
|
profiles, err := km.fetchAllUserProfiles()
|
||||||
q.Add("q", wtAccountID+":"+accountID)
|
|
||||||
|
|
||||||
body, err := km.get("users", q)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -352,15 +270,12 @@ func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
km.appMetrics.IDPMetrics().CountGetAccount()
|
km.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles := make([]keycloakProfile, 0)
|
|
||||||
err = km.helper.Unmarshal(body, &profiles)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0)
|
||||||
for _, profile := range profiles {
|
for _, profile := range profiles {
|
||||||
users = append(users, profile.userData())
|
userData := profile.userData()
|
||||||
|
userData.AppMetadata.WTAccountID = accountID
|
||||||
|
|
||||||
|
users = append(users, userData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
@@ -369,6 +284,76 @@ func (km *KeycloakManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (km *KeycloakManager) GetAllAccounts() (map[string][]*UserData, error) {
|
func (km *KeycloakManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
|
profiles, err := km.fetchAllUserProfiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if km.appMetrics != nil {
|
||||||
|
km.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
indexedUsers := make(map[string][]*UserData)
|
||||||
|
for _, profile := range profiles {
|
||||||
|
userData := profile.userData()
|
||||||
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexedUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
|
func (km *KeycloakManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteUserByID resend invitations to users who haven't activated,
|
||||||
|
// their accounts prior to the expiration period.
|
||||||
|
func (km *KeycloakManager) InviteUserByID(_ string) error {
|
||||||
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser from Keycloak by user ID.
|
||||||
|
func (km *KeycloakManager) DeleteUser(userID string) error {
|
||||||
|
jwtToken, err := km.credentials.Authenticate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := fmt.Sprintf("%s/users/%s", km.adminEndpoint, url.QueryEscape(userID))
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
||||||
|
req.Header.Add("content-type", "application/json")
|
||||||
|
|
||||||
|
if km.appMetrics != nil {
|
||||||
|
km.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := km.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if km.appMetrics != nil {
|
||||||
|
km.appMetrics.IDPMetrics().CountRequestError()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close() // nolint
|
||||||
|
|
||||||
|
// In the docs, they specified 200, but in the endpoints, they return 204
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
if km.appMetrics != nil {
|
||||||
|
km.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (km *KeycloakManager) fetchAllUserProfiles() ([]keycloakProfile, error) {
|
||||||
totalUsers, err := km.totalUsersCount()
|
totalUsers, err := km.totalUsersCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -382,117 +367,13 @@ func (km *KeycloakManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountGetAllAccounts()
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles := make([]keycloakProfile, 0)
|
profiles := make([]keycloakProfile, 0)
|
||||||
err = km.helper.Unmarshal(body, &profiles)
|
err = km.helper.Unmarshal(body, &profiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
return profiles, nil
|
||||||
for _, profile := range profiles {
|
|
||||||
userData := profile.userData()
|
|
||||||
|
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
|
||||||
if accountID != "" {
|
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return indexedUsers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
|
||||||
func (km *KeycloakManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
|
||||||
jwtToken, err := km.credentials.Authenticate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs := keycloakUserAttributes{}
|
|
||||||
attrs.Set(wtAccountID, appMetadata.WTAccountID)
|
|
||||||
if appMetadata.WTPendingInvite != nil {
|
|
||||||
attrs.Set(wtPendingInvite, strconv.FormatBool(*appMetadata.WTPendingInvite))
|
|
||||||
} else {
|
|
||||||
attrs.Set(wtPendingInvite, "false")
|
|
||||||
}
|
|
||||||
|
|
||||||
reqURL := fmt.Sprintf("%s/users/%s", km.adminEndpoint, userID)
|
|
||||||
data, err := km.helper.Marshal(map[string]any{
|
|
||||||
"attributes": attrs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
payload := strings.NewReader(string(data))
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPut, reqURL, payload)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("authorization", "Bearer "+jwtToken.AccessToken)
|
|
||||||
req.Header.Add("content-type", "application/json")
|
|
||||||
|
|
||||||
log.Debugf("updating IdP metadata for user %s", userID)
|
|
||||||
|
|
||||||
resp, err := km.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountRequestError()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if km.appMetrics != nil {
|
|
||||||
km.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
|
||||||
return fmt.Errorf("unable to update the appMetadata, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InviteUserByID resend invitations to users who haven't activated,
|
|
||||||
// their accounts prior to the expiration period.
|
|
||||||
func (km *KeycloakManager) InviteUserByID(_ string) error {
|
|
||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildKeycloakCreateUserRequestPayload(email string, name string, appMetadata AppMetadata) (string, error) {
|
|
||||||
attrs := keycloakUserAttributes{}
|
|
||||||
attrs.Set(wtAccountID, appMetadata.WTAccountID)
|
|
||||||
attrs.Set(wtPendingInvite, strconv.FormatBool(*appMetadata.WTPendingInvite))
|
|
||||||
|
|
||||||
req := &keycloakCreateUserRequest{
|
|
||||||
Email: email,
|
|
||||||
Username: name,
|
|
||||||
Enabled: true,
|
|
||||||
EmailVerified: true,
|
|
||||||
Credentials: []keycloakUserCredential{
|
|
||||||
{
|
|
||||||
Type: "password",
|
|
||||||
Value: GeneratePassword(8, 1, 1, 1),
|
|
||||||
Temporary: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Attributes: attrs,
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(str), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get perform Get requests.
|
// get perform Get requests.
|
||||||
@@ -547,53 +428,11 @@ func (km *KeycloakManager) totalUsersCount() (*int, error) {
|
|||||||
return &count, nil
|
return &count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractUserIDFromLocationHeader extracts the user ID from the location,
|
|
||||||
// header once the user is created successfully
|
|
||||||
func extractUserIDFromLocationHeader(locationHeader string) (string, error) {
|
|
||||||
userURL, err := url.Parse(locationHeader)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Base(userURL.Path), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// userData construct user data from keycloak profile.
|
// userData construct user data from keycloak profile.
|
||||||
func (kp keycloakProfile) userData() *UserData {
|
func (kp keycloakProfile) userData() *UserData {
|
||||||
accountID := kp.Attributes.Get(wtAccountID)
|
|
||||||
pendingInvite, err := strconv.ParseBool(kp.Attributes.Get(wtPendingInvite))
|
|
||||||
if err != nil {
|
|
||||||
pendingInvite = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return &UserData{
|
return &UserData{
|
||||||
Email: kp.Email,
|
Email: kp.Email,
|
||||||
Name: kp.Username,
|
Name: kp.Username,
|
||||||
ID: kp.ID,
|
ID: kp.ID,
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: accountID,
|
|
||||||
WTPendingInvite: &pendingInvite,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the key to value. It replaces any existing
|
|
||||||
// values.
|
|
||||||
func (ka keycloakUserAttributes) Set(key, value string) {
|
|
||||||
ka[key] = []string{value}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the first value associated with the given key.
|
|
||||||
// If there are no values associated with the key, Get returns
|
|
||||||
// the empty string.
|
|
||||||
func (ka keycloakUserAttributes) Get(key string) string {
|
|
||||||
if ka == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
values := ka[key]
|
|
||||||
if len(values) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return values[0]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -84,15 +84,6 @@ func TestNewKeycloakManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockKeycloakCredentials struct {
|
|
||||||
jwtToken JWTToken
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *mockKeycloakCredentials) Authenticate() (JWTToken, error) {
|
|
||||||
return mc.jwtToken, mc.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeycloakRequestJWTToken(t *testing.T) {
|
func TestKeycloakRequestJWTToken(t *testing.T) {
|
||||||
|
|
||||||
type requestJWTTokenTest struct {
|
type requestJWTTokenTest struct {
|
||||||
@@ -316,108 +307,3 @@ func TestKeycloakAuthenticate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestKeycloakUpdateUserAppMetadata(t *testing.T) {
|
|
||||||
type updateUserAppMetadataTest struct {
|
|
||||||
name string
|
|
||||||
inputReqBody string
|
|
||||||
expectedReqBody string
|
|
||||||
appMetadata AppMetadata
|
|
||||||
statusCode int
|
|
||||||
helper ManagerHelper
|
|
||||||
managerCreds ManagerCredentials
|
|
||||||
assertErrFunc assert.ErrorAssertionFunc
|
|
||||||
assertErrFuncMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Authentication",
|
|
||||||
expectedReqBody: "",
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 400,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockKeycloakCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
err: fmt.Errorf("error"),
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Status Code",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"false\"]}}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 400,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockKeycloakCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Response Parsing",
|
|
||||||
statusCode: 400,
|
|
||||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
|
||||||
managerCreds: &mockKeycloakCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
|
||||||
name: "Good request",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"false\"]}}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 204,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockKeycloakCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
invite := true
|
|
||||||
updateUserAppMetadataTestCase5 := updateUserAppMetadataTest{
|
|
||||||
name: "Update Pending Invite",
|
|
||||||
expectedReqBody: fmt.Sprintf("{\"attributes\":{\"wt_account_id\":[\"%s\"],\"wt_pending_invite\":[\"true\"]}}", appMetadata.WTAccountID),
|
|
||||||
appMetadata: AppMetadata{
|
|
||||||
WTAccountID: "ok",
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
},
|
|
||||||
statusCode: 204,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockKeycloakCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
|
||||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4, updateUserAppMetadataTestCase5} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
reqClient := mockHTTPClient{
|
|
||||||
resBody: testCase.inputReqBody,
|
|
||||||
code: testCase.statusCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := &KeycloakManager{
|
|
||||||
httpClient: &reqClient,
|
|
||||||
credentials: testCase.managerCreds,
|
|
||||||
helper: testCase.helper,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
|
||||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
"github.com/okta/okta-sdk-golang/v2/okta"
|
"github.com/okta/okta-sdk-golang/v2/okta"
|
||||||
"github.com/okta/okta-sdk-golang/v2/okta/query"
|
"github.com/okta/okta-sdk-golang/v2/okta/query"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OktaManager okta manager client instance.
|
// OktaManager okta manager client instance.
|
||||||
@@ -76,11 +77,6 @@ func NewOktaManager(config OktaClientConfig, appMetrics telemetry.AppMetrics) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = updateUserProfileSchema(client)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials := &OktaCredentials{
|
credentials := &OktaCredentials{
|
||||||
clientConfig: config,
|
clientConfig: config,
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
@@ -103,49 +99,8 @@ func (oc *OktaCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in okta Idp and sends an invitation.
|
// CreateUser creates a new user in okta Idp and sends an invitation.
|
||||||
func (om *OktaManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (om *OktaManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
var (
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
sendEmail = true
|
|
||||||
activate = true
|
|
||||||
userProfile = okta.UserProfile{
|
|
||||||
"email": email,
|
|
||||||
"login": email,
|
|
||||||
wtAccountID: accountID,
|
|
||||||
wtPendingInvite: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
fields := strings.Fields(name)
|
|
||||||
if n := len(fields); n > 0 {
|
|
||||||
userProfile["firstName"] = strings.Join(fields[:n-1], " ")
|
|
||||||
userProfile["lastName"] = fields[n-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
user, resp, err := om.client.User.CreateUser(context.Background(),
|
|
||||||
okta.CreateUserRequest{
|
|
||||||
Profile: &userProfile,
|
|
||||||
},
|
|
||||||
&query.Params{
|
|
||||||
Activate: &activate,
|
|
||||||
SendEmail: &sendEmail,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if om.appMetrics != nil {
|
|
||||||
om.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
if om.appMetrics != nil {
|
|
||||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to create user, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseOktaUser(user)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserDataByID requests user data from keycloak via ID.
|
// GetUserDataByID requests user data from keycloak via ID.
|
||||||
@@ -166,7 +121,13 @@ func (om *OktaManager) GetUserDataByID(userID string, appMetadata AppMetadata) (
|
|||||||
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseOktaUser(user)
|
userData, err := parseOktaUser(user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userData.AppMetadata = appMetadata
|
||||||
|
|
||||||
|
return userData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
@@ -200,8 +161,7 @@ func (om *OktaManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
search := fmt.Sprintf("profile.wt_account_id eq %q", accountID)
|
users, err := om.getAllUsers()
|
||||||
users, resp, err := om.client.User.ListUsers(context.Background(), &query.Params{Search: search})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,38 +170,40 @@ func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
om.appMetrics.IDPMetrics().CountGetAccount()
|
om.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
for index, user := range users {
|
||||||
if om.appMetrics != nil {
|
user.AppMetadata.WTAccountID = accountID
|
||||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
users[index] = user
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to get account, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
list := make([]*UserData, 0)
|
return users, nil
|
||||||
for _, user := range users {
|
|
||||||
userData, err := parseOktaUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
list = append(list, userData)
|
|
||||||
}
|
|
||||||
|
|
||||||
return list, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
// It returns a list of users indexed by accountID.
|
// It returns a list of users indexed by accountID.
|
||||||
func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
||||||
users, resp, err := om.client.User.ListUsers(context.Background(), nil)
|
users, err := om.getAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indexedUsers := make(map[string][]*UserData)
|
||||||
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...)
|
||||||
|
|
||||||
if om.appMetrics != nil {
|
if om.appMetrics != nil {
|
||||||
om.appMetrics.IDPMetrics().CountGetAllAccounts()
|
om.appMetrics.IDPMetrics().CountGetAllAccounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return indexedUsers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAllUsers returns all users in an Okta account.
|
||||||
|
func (om *OktaManager) getAllUsers() ([]*UserData, error) {
|
||||||
|
qp := query.NewQueryParams(query.WithLimit(200))
|
||||||
|
userList, resp, err := om.client.User.ListUsers(context.Background(), qp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
if om.appMetrics != nil {
|
if om.appMetrics != nil {
|
||||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
@@ -249,67 +211,38 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
for resp.HasNextPage() {
|
||||||
for _, user := range users {
|
paginatedUsers := make([]*okta.User, 0)
|
||||||
|
resp, err = resp.Next(context.Background(), &paginatedUsers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if om.appMetrics != nil {
|
||||||
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
userList = append(userList, paginatedUsers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make([]*UserData, 0, len(userList))
|
||||||
|
for _, user := range userList {
|
||||||
userData, err := parseOktaUser(user)
|
userData, err := parseOktaUser(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
users = append(users, userData)
|
||||||
if accountID != "" {
|
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedUsers, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
func (om *OktaManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
func (om *OktaManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
||||||
user, resp, err := om.client.User.GetUser(context.Background(), userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
if om.appMetrics != nil {
|
|
||||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
profile := *user.Profile
|
|
||||||
|
|
||||||
if appMetadata.WTPendingInvite != nil {
|
|
||||||
profile[wtPendingInvite] = *appMetadata.WTPendingInvite
|
|
||||||
}
|
|
||||||
|
|
||||||
if appMetadata.WTAccountID != "" {
|
|
||||||
profile[wtAccountID] = appMetadata.WTAccountID
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Profile = &profile
|
|
||||||
_, resp, err = om.client.User.UpdateUser(context.Background(), userID, *user, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if om.appMetrics != nil {
|
|
||||||
om.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
if om.appMetrics != nil {
|
|
||||||
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to update user, statusCode %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,47 +252,23 @@ func (om *OktaManager) InviteUserByID(_ string) error {
|
|||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateUserProfileSchema updates the Okta user schema to include custom fields,
|
// DeleteUser from Okta
|
||||||
// wt_account_id and wt_pending_invite.
|
func (om *OktaManager) DeleteUser(userID string) error {
|
||||||
func updateUserProfileSchema(client *okta.Client) error {
|
resp, err := om.client.User.DeactivateOrDeleteUser(context.Background(), userID, nil)
|
||||||
// Ensure Okta doesn't enforce user input for these fields, as they are solely used by Netbird
|
|
||||||
userPermissions := []*okta.UserSchemaAttributePermission{{Action: "HIDE", Principal: "SELF"}}
|
|
||||||
|
|
||||||
_, resp, err := client.UserSchema.UpdateUserProfile(
|
|
||||||
context.Background(),
|
|
||||||
"default",
|
|
||||||
okta.UserSchema{
|
|
||||||
Definitions: &okta.UserSchemaDefinitions{
|
|
||||||
Custom: &okta.UserSchemaPublic{
|
|
||||||
Id: "#custom",
|
|
||||||
Type: "object",
|
|
||||||
Properties: map[string]*okta.UserSchemaAttribute{
|
|
||||||
wtAccountID: {
|
|
||||||
MaxLength: 100,
|
|
||||||
MinLength: 1,
|
|
||||||
Required: new(bool),
|
|
||||||
Scope: "NONE",
|
|
||||||
Title: "Wt Account Id",
|
|
||||||
Type: "string",
|
|
||||||
Permissions: userPermissions,
|
|
||||||
},
|
|
||||||
wtPendingInvite: {
|
|
||||||
Required: new(bool),
|
|
||||||
Scope: "NONE",
|
|
||||||
Title: "Wt Pending Invite",
|
|
||||||
Type: "boolean",
|
|
||||||
Permissions: userPermissions,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if om.appMetrics != nil {
|
||||||
|
om.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("unable to update user profile schema, statusCode %d", resp.StatusCode)
|
if om.appMetrics != nil {
|
||||||
|
om.appMetrics.IDPMetrics().CountRequestStatusError()
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -368,11 +277,9 @@ func updateUserProfileSchema(client *okta.Client) error {
|
|||||||
// parseOktaUserToUserData parse okta user to UserData.
|
// parseOktaUserToUserData parse okta user to UserData.
|
||||||
func parseOktaUser(user *okta.User) (*UserData, error) {
|
func parseOktaUser(user *okta.User) (*UserData, error) {
|
||||||
var oktaUser struct {
|
var oktaUser struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName string `json:"lastName"`
|
||||||
AccountID string `json:"wt_account_id"`
|
|
||||||
PendingInvite bool `json:"wt_pending_invite"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
@@ -396,9 +303,5 @@ func parseOktaUser(user *okta.User) (*UserData, error) {
|
|||||||
Email: oktaUser.Email,
|
Email: oktaUser.Email,
|
||||||
Name: strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
|
Name: strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
|
||||||
ID: user.Id,
|
ID: user.Id,
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: oktaUser.AccountID,
|
|
||||||
WTPendingInvite: &oktaUser.PendingInvite,
|
|
||||||
},
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/okta/okta-sdk-golang/v2/okta"
|
"github.com/okta/okta-sdk-golang/v2/okta"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseOktaUser(t *testing.T) {
|
func TestParseOktaUser(t *testing.T) {
|
||||||
type parseOktaUserTest struct {
|
type parseOktaUserTest struct {
|
||||||
name string
|
name string
|
||||||
invite bool
|
|
||||||
inputProfile *okta.User
|
inputProfile *okta.User
|
||||||
expectedUserData *UserData
|
expectedUserData *UserData
|
||||||
assertErrFunc assert.ErrorAssertionFunc
|
assertErrFunc assert.ErrorAssertionFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
parseOktaTestCase1 := parseOktaUserTest{
|
parseOktaTestCase1 := parseOktaUserTest{
|
||||||
name: "Good Request",
|
name: "Good Request",
|
||||||
invite: true,
|
|
||||||
inputProfile: &okta.User{
|
inputProfile: &okta.User{
|
||||||
Id: "123",
|
Id: "123",
|
||||||
Profile: &okta.UserProfile{
|
Profile: &okta.UserProfile{
|
||||||
"email": "test@example.com",
|
"email": "test@example.com",
|
||||||
"firstName": "John",
|
"firstName": "John",
|
||||||
"lastName": "Doe",
|
"lastName": "Doe",
|
||||||
"wt_account_id": "456",
|
|
||||||
"wt_pending_invite": true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUserData: &UserData{
|
expectedUserData: &UserData{
|
||||||
@@ -41,36 +38,17 @@ func TestParseOktaUser(t *testing.T) {
|
|||||||
|
|
||||||
parseOktaTestCase2 := parseOktaUserTest{
|
parseOktaTestCase2 := parseOktaUserTest{
|
||||||
name: "Invalid okta user",
|
name: "Invalid okta user",
|
||||||
invite: true,
|
|
||||||
inputProfile: nil,
|
inputProfile: nil,
|
||||||
expectedUserData: nil,
|
expectedUserData: nil,
|
||||||
assertErrFunc: assert.Error,
|
assertErrFunc: assert.Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
parseOktaTestCase3 := parseOktaUserTest{
|
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2} {
|
||||||
name: "Invalid pending invite type",
|
|
||||||
invite: false,
|
|
||||||
inputProfile: &okta.User{
|
|
||||||
Id: "123",
|
|
||||||
Profile: &okta.UserProfile{
|
|
||||||
"email": "test@example.com",
|
|
||||||
"firstName": "John",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"wt_account_id": "456",
|
|
||||||
"wt_pending_invite": "true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedUserData: nil,
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []parseOktaUserTest{parseOktaTestCase1, parseOktaTestCase2, parseOktaTestCase3} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
userData, err := parseOktaUser(testCase.inputProfile)
|
userData, err := parseOktaUser(testCase.inputProfile)
|
||||||
testCase.assertErrFunc(t, err, testCase.assertErrFunc)
|
testCase.assertErrFunc(t, err, testCase.assertErrFunc)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
testCase.expectedUserData.AppMetadata.WTPendingInvite = &testCase.invite
|
|
||||||
assert.True(t, userDataEqual(testCase.expectedUserData, userData), "user data should match")
|
assert.True(t, userDataEqual(testCase.expectedUserData, userData), "user data should match")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -83,13 +61,5 @@ func userDataEqual(a, b *UserData) bool {
|
|||||||
if a.Email != b.Email || a.Name != b.Name || a.ID != b.ID {
|
if a.Email != b.Email || a.Name != b.Name || a.ID != b.ID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if a.AppMetadata.WTAccountID != b.AppMetadata.WTAccountID {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.AppMetadata.WTPendingInvite != nil && b.AppMetadata.WTPendingInvite != nil &&
|
|
||||||
*a.AppMetadata.WTPendingInvite != *b.AppMetadata.WTPendingInvite {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
package idp
|
package idp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt"
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ZitadelManager zitadel manager client instance.
|
// ZitadelManager zitadel manager client instance.
|
||||||
@@ -67,12 +65,6 @@ type zitadelUser struct {
|
|||||||
|
|
||||||
type zitadelAttributes map[string][]map[string]any
|
type zitadelAttributes map[string][]map[string]any
|
||||||
|
|
||||||
// zitadelMetadata holds additional user data.
|
|
||||||
type zitadelMetadata struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// zitadelProfile represents an zitadel user profile response.
|
// zitadelProfile represents an zitadel user profile response.
|
||||||
type zitadelProfile struct {
|
type zitadelProfile struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -81,7 +73,6 @@ type zitadelProfile struct {
|
|||||||
PreferredLoginName string `json:"preferredLoginName"`
|
PreferredLoginName string `json:"preferredLoginName"`
|
||||||
LoginNames []string `json:"loginNames"`
|
LoginNames []string `json:"loginNames"`
|
||||||
Human *zitadelUser `json:"human"`
|
Human *zitadelUser `json:"human"`
|
||||||
Metadata []zitadelMetadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewZitadelManager creates a new instance of the ZitadelManager.
|
// NewZitadelManager creates a new instance of the ZitadelManager.
|
||||||
@@ -234,42 +225,8 @@ func (zc *ZitadelCredentials) Authenticate() (JWTToken, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user in zitadel Idp and sends an invite.
|
// CreateUser creates a new user in zitadel Idp and sends an invite.
|
||||||
func (zm *ZitadelManager) CreateUser(email, name, accountID, invitedByEmail string) (*UserData, error) {
|
func (zm *ZitadelManager) CreateUser(_, _, _, _ string) (*UserData, error) {
|
||||||
payload, err := buildZitadelCreateUserRequestPayload(email, name)
|
return nil, fmt.Errorf("method CreateUser not implemented")
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := zm.post("users/human/_import", payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if zm.appMetrics != nil {
|
|
||||||
zm.appMetrics.IDPMetrics().CountCreateUser()
|
|
||||||
}
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
UserID string `json:"userId"`
|
|
||||||
}
|
|
||||||
err = zm.helper.Unmarshal(body, &result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
invite := true
|
|
||||||
appMetadata := AppMetadata{
|
|
||||||
WTAccountID: accountID,
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add metadata to new user
|
|
||||||
err = zm.UpdateUserAppMetadata(result.UserID, appMetadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return zm.GetUserDataByID(result.UserID, appMetadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail searches users with a given email.
|
// GetUserByEmail searches users with a given email.
|
||||||
@@ -307,12 +264,6 @@ func (zm *ZitadelManager) GetUserByEmail(email string) ([]*UserData, error) {
|
|||||||
|
|
||||||
users := make([]*UserData, 0)
|
users := make([]*UserData, 0)
|
||||||
for _, profile := range profiles.Result {
|
for _, profile := range profiles.Result {
|
||||||
metadata, err := zm.getUserMetadata(profile.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile.Metadata = metadata
|
|
||||||
|
|
||||||
users = append(users, profile.userData())
|
users = append(users, profile.userData())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,18 +287,15 @@ func (zm *ZitadelManager) GetUserDataByID(userID string, appMetadata AppMetadata
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata, err := zm.getUserMetadata(userID)
|
userData := profile.User.userData()
|
||||||
if err != nil {
|
userData.AppMetadata = appMetadata
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile.User.Metadata = metadata
|
|
||||||
|
|
||||||
return profile.User.userData(), nil
|
return userData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount returns all the users for a given profile.
|
// GetAccount returns all the users for a given profile.
|
||||||
func (zm *ZitadelManager) GetAccount(accountID string) ([]*UserData, error) {
|
func (zm *ZitadelManager) GetAccount(accountID string) ([]*UserData, error) {
|
||||||
accounts, err := zm.GetAllAccounts()
|
body, err := zm.post("users/_search", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -356,7 +304,21 @@ func (zm *ZitadelManager) GetAccount(accountID string) ([]*UserData, error) {
|
|||||||
zm.appMetrics.IDPMetrics().CountGetAccount()
|
zm.appMetrics.IDPMetrics().CountGetAccount()
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts[accountID], nil
|
var profiles struct{ Result []zitadelProfile }
|
||||||
|
err = zm.helper.Unmarshal(body, &profiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
users := make([]*UserData, 0)
|
||||||
|
for _, profile := range profiles.Result {
|
||||||
|
userData := profile.userData()
|
||||||
|
userData.AppMetadata.WTAccountID = accountID
|
||||||
|
|
||||||
|
users = append(users, userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAccounts gets all registered accounts with corresponding user data.
|
// GetAllAccounts gets all registered accounts with corresponding user data.
|
||||||
@@ -379,22 +341,8 @@ func (zm *ZitadelManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
|
|
||||||
indexedUsers := make(map[string][]*UserData)
|
indexedUsers := make(map[string][]*UserData)
|
||||||
for _, profile := range profiles.Result {
|
for _, profile := range profiles.Result {
|
||||||
// fetch user metadata
|
|
||||||
metadata, err := zm.getUserMetadata(profile.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
profile.Metadata = metadata
|
|
||||||
|
|
||||||
userData := profile.userData()
|
userData := profile.userData()
|
||||||
accountID := userData.AppMetadata.WTAccountID
|
indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData)
|
||||||
|
|
||||||
if accountID != "" {
|
|
||||||
if _, ok := indexedUsers[accountID]; !ok {
|
|
||||||
indexedUsers[accountID] = make([]*UserData, 0)
|
|
||||||
}
|
|
||||||
indexedUsers[accountID] = append(indexedUsers[accountID], userData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return indexedUsers, nil
|
return indexedUsers, nil
|
||||||
@@ -402,42 +350,7 @@ func (zm *ZitadelManager) GetAllAccounts() (map[string][]*UserData, error) {
|
|||||||
|
|
||||||
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
|
||||||
// Metadata values are base64 encoded.
|
// Metadata values are base64 encoded.
|
||||||
func (zm *ZitadelManager) UpdateUserAppMetadata(userID string, appMetadata AppMetadata) error {
|
func (zm *ZitadelManager) UpdateUserAppMetadata(_ string, _ AppMetadata) error {
|
||||||
if appMetadata.WTPendingInvite == nil {
|
|
||||||
appMetadata.WTPendingInvite = new(bool)
|
|
||||||
}
|
|
||||||
pendingInviteBuf := strconv.AppendBool([]byte{}, *appMetadata.WTPendingInvite)
|
|
||||||
|
|
||||||
wtAccountIDValue := base64.StdEncoding.EncodeToString([]byte(appMetadata.WTAccountID))
|
|
||||||
wtPendingInviteValue := base64.StdEncoding.EncodeToString(pendingInviteBuf)
|
|
||||||
|
|
||||||
metadata := zitadelAttributes{
|
|
||||||
"metadata": {
|
|
||||||
{
|
|
||||||
"key": wtAccountID,
|
|
||||||
"value": wtAccountIDValue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": wtPendingInvite,
|
|
||||||
"value": wtPendingInviteValue,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
payload, err := zm.helper.Marshal(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resource := fmt.Sprintf("users/%s/metadata/_bulk", userID)
|
|
||||||
_, err = zm.post(resource, string(payload))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if zm.appMetrics != nil {
|
|
||||||
zm.appMetrics.IDPMetrics().CountUpdateUserAppMetadata()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,21 +360,18 @@ func (zm *ZitadelManager) InviteUserByID(_ string) error {
|
|||||||
return fmt.Errorf("method InviteUserByID not implemented")
|
return fmt.Errorf("method InviteUserByID not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUserMetadata requests user metadata from zitadel via ID.
|
// DeleteUser from Zitadel
|
||||||
func (zm *ZitadelManager) getUserMetadata(userID string) ([]zitadelMetadata, error) {
|
func (zm *ZitadelManager) DeleteUser(userID string) error {
|
||||||
resource := fmt.Sprintf("users/%s/metadata/_search", userID)
|
resource := fmt.Sprintf("users/%s", userID)
|
||||||
body, err := zm.post(resource, "")
|
if err := zm.delete(resource); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata struct{ Result []zitadelMetadata }
|
if zm.appMetrics != nil {
|
||||||
err = zm.helper.Unmarshal(body, &metadata)
|
zm.appMetrics.IDPMetrics().CountDeleteUser()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata.Result, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// post perform Post requests.
|
// post perform Post requests.
|
||||||
@@ -500,6 +410,11 @@ func (zm *ZitadelManager) post(resource string, body string) ([]byte, error) {
|
|||||||
return io.ReadAll(resp.Body)
|
return io.ReadAll(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete perform Delete requests.
|
||||||
|
func (zm *ZitadelManager) delete(_ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// get perform Get requests.
|
// get perform Get requests.
|
||||||
func (zm *ZitadelManager) get(resource string, q url.Values) ([]byte, error) {
|
func (zm *ZitadelManager) get(resource string, q url.Values) ([]byte, error) {
|
||||||
jwtToken, err := zm.credentials.Authenticate()
|
jwtToken, err := zm.credentials.Authenticate()
|
||||||
@@ -536,38 +451,13 @@ func (zm *ZitadelManager) get(resource string, q url.Values) ([]byte, error) {
|
|||||||
return io.ReadAll(resp.Body)
|
return io.ReadAll(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// value returns string represented by the base64 string value.
|
|
||||||
func (zm zitadelMetadata) value() string {
|
|
||||||
value, err := base64.StdEncoding.DecodeString(zm.Value)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// userData construct user data from zitadel profile.
|
// userData construct user data from zitadel profile.
|
||||||
func (zp zitadelProfile) userData() *UserData {
|
func (zp zitadelProfile) userData() *UserData {
|
||||||
var (
|
var (
|
||||||
email string
|
email string
|
||||||
name string
|
name string
|
||||||
wtAccountIDValue string
|
|
||||||
wtPendingInviteValue bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, metadata := range zp.Metadata {
|
|
||||||
if metadata.Key == wtAccountID {
|
|
||||||
wtAccountIDValue = metadata.value()
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.Key == wtPendingInvite {
|
|
||||||
value, err := strconv.ParseBool(metadata.value())
|
|
||||||
if err == nil {
|
|
||||||
wtPendingInviteValue = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain the email for the human account and the login name,
|
// Obtain the email for the human account and the login name,
|
||||||
// for the machine account.
|
// for the machine account.
|
||||||
if zp.Human != nil {
|
if zp.Human != nil {
|
||||||
@@ -584,39 +474,5 @@ func (zp zitadelProfile) userData() *UserData {
|
|||||||
Email: email,
|
Email: email,
|
||||||
Name: name,
|
Name: name,
|
||||||
ID: zp.ID,
|
ID: zp.ID,
|
||||||
AppMetadata: AppMetadata{
|
|
||||||
WTAccountID: wtAccountIDValue,
|
|
||||||
WTPendingInvite: &wtPendingInviteValue,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildZitadelCreateUserRequestPayload(email string, name string) (string, error) {
|
|
||||||
var firstName, lastName string
|
|
||||||
|
|
||||||
words := strings.Fields(name)
|
|
||||||
if n := len(words); n > 0 {
|
|
||||||
firstName = strings.Join(words[:n-1], " ")
|
|
||||||
lastName = words[n-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &zitadelUser{
|
|
||||||
UserName: name,
|
|
||||||
Profile: zitadelUserInfo{
|
|
||||||
FirstName: strings.TrimSpace(firstName),
|
|
||||||
LastName: strings.TrimSpace(lastName),
|
|
||||||
DisplayName: name,
|
|
||||||
},
|
|
||||||
Email: zitadelEmail{
|
|
||||||
Email: email,
|
|
||||||
IsEmailVerified: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(str), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewZitadelManager(t *testing.T) {
|
func TestNewZitadelManager(t *testing.T) {
|
||||||
@@ -63,15 +64,6 @@ func TestNewZitadelManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockZitadelCredentials struct {
|
|
||||||
jwtToken JWTToken
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mc *mockZitadelCredentials) Authenticate() (JWTToken, error) {
|
|
||||||
return mc.jwtToken, mc.err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZitadelRequestJWTToken(t *testing.T) {
|
func TestZitadelRequestJWTToken(t *testing.T) {
|
||||||
|
|
||||||
type requestJWTTokenTest struct {
|
type requestJWTTokenTest struct {
|
||||||
@@ -296,98 +288,6 @@ func TestZitadelAuthenticate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestZitadelUpdateUserAppMetadata(t *testing.T) {
|
|
||||||
type updateUserAppMetadataTest struct {
|
|
||||||
name string
|
|
||||||
inputReqBody string
|
|
||||||
expectedReqBody string
|
|
||||||
appMetadata AppMetadata
|
|
||||||
statusCode int
|
|
||||||
helper ManagerHelper
|
|
||||||
managerCreds ManagerCredentials
|
|
||||||
assertErrFunc assert.ErrorAssertionFunc
|
|
||||||
assertErrFuncMessage string
|
|
||||||
}
|
|
||||||
|
|
||||||
appMetadata := AppMetadata{WTAccountID: "ok"}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase1 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Authentication",
|
|
||||||
expectedReqBody: "",
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 400,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockZitadelCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
err: fmt.Errorf("error"),
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase2 := updateUserAppMetadataTest{
|
|
||||||
name: "Bad Response Parsing",
|
|
||||||
statusCode: 400,
|
|
||||||
helper: &mockJsonParser{marshalErrorString: "error"},
|
|
||||||
managerCreds: &mockZitadelCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.Error,
|
|
||||||
assertErrFuncMessage: "should return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserAppMetadataTestCase3 := updateUserAppMetadataTest{
|
|
||||||
name: "Good request",
|
|
||||||
expectedReqBody: "{\"metadata\":[{\"key\":\"wt_account_id\",\"value\":\"b2s=\"},{\"key\":\"wt_pending_invite\",\"value\":\"ZmFsc2U=\"}]}",
|
|
||||||
appMetadata: appMetadata,
|
|
||||||
statusCode: 200,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockZitadelCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
invite := true
|
|
||||||
updateUserAppMetadataTestCase4 := updateUserAppMetadataTest{
|
|
||||||
name: "Update Pending Invite",
|
|
||||||
expectedReqBody: "{\"metadata\":[{\"key\":\"wt_account_id\",\"value\":\"b2s=\"},{\"key\":\"wt_pending_invite\",\"value\":\"dHJ1ZQ==\"}]}",
|
|
||||||
appMetadata: AppMetadata{
|
|
||||||
WTAccountID: "ok",
|
|
||||||
WTPendingInvite: &invite,
|
|
||||||
},
|
|
||||||
statusCode: 200,
|
|
||||||
helper: JsonParser{},
|
|
||||||
managerCreds: &mockZitadelCredentials{
|
|
||||||
jwtToken: JWTToken{},
|
|
||||||
},
|
|
||||||
assertErrFunc: assert.NoError,
|
|
||||||
assertErrFuncMessage: "shouldn't return error",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range []updateUserAppMetadataTest{updateUserAppMetadataTestCase1, updateUserAppMetadataTestCase2,
|
|
||||||
updateUserAppMetadataTestCase3, updateUserAppMetadataTestCase4} {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
reqClient := mockHTTPClient{
|
|
||||||
resBody: testCase.inputReqBody,
|
|
||||||
code: testCase.statusCode,
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := &ZitadelManager{
|
|
||||||
httpClient: &reqClient,
|
|
||||||
credentials: testCase.managerCreds,
|
|
||||||
helper: testCase.helper,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := manager.UpdateUserAppMetadata("1", testCase.appMetadata)
|
|
||||||
testCase.assertErrFunc(t, err, testCase.assertErrFuncMessage)
|
|
||||||
|
|
||||||
assert.Equal(t, testCase.expectedReqBody, reqClient.reqBody, "request body should match")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestZitadelProfile(t *testing.T) {
|
func TestZitadelProfile(t *testing.T) {
|
||||||
type azureProfileTest struct {
|
type azureProfileTest struct {
|
||||||
name string
|
name string
|
||||||
@@ -418,16 +318,6 @@ func TestZitadelProfile(t *testing.T) {
|
|||||||
IsEmailVerified: true,
|
IsEmailVerified: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Metadata: []zitadelMetadata{
|
|
||||||
{
|
|
||||||
Key: "wt_account_id",
|
|
||||||
Value: "MQ==",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "wt_pending_invite",
|
|
||||||
Value: "ZmFsc2U=",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expectedUserData: UserData{
|
expectedUserData: UserData{
|
||||||
ID: "test1",
|
ID: "test1",
|
||||||
@@ -451,16 +341,6 @@ func TestZitadelProfile(t *testing.T) {
|
|||||||
"machine",
|
"machine",
|
||||||
},
|
},
|
||||||
Human: nil,
|
Human: nil,
|
||||||
Metadata: []zitadelMetadata{
|
|
||||||
{
|
|
||||||
Key: "wt_account_id",
|
|
||||||
Value: "MQ==",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "wt_pending_invite",
|
|
||||||
Value: "dHJ1ZQ==",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
expectedUserData: UserData{
|
expectedUserData: UserData{
|
||||||
ID: "test2",
|
ID: "test2",
|
||||||
@@ -480,8 +360,6 @@ func TestZitadelProfile(t *testing.T) {
|
|||||||
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
assert.Equal(t, testCase.expectedUserData.ID, userData.ID, "User id should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
assert.Equal(t, testCase.expectedUserData.Email, userData.Email, "User email should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
assert.Equal(t, testCase.expectedUserData.Name, userData.Name, "User name should match")
|
||||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTAccountID, userData.AppMetadata.WTAccountID, "Account id should match")
|
|
||||||
assert.Equal(t, testCase.expectedUserData.AppMetadata.WTPendingInvite, userData.AppMetadata.WTPendingInvite, "Pending invite should match")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error)
|
|||||||
peersUpdateManager := NewPeersUpdateManager()
|
peersUpdateManager := NewPeersUpdateManager()
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "",
|
accountManager, err := BuildManager(store, peersUpdateManager, nil, "", "",
|
||||||
eventStore)
|
eventStore, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
|
|||||||
peersUpdateManager := server.NewPeersUpdateManager()
|
peersUpdateManager := server.NewPeersUpdateManager()
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
accountManager, err := server.BuildManager(store, peersUpdateManager, nil, "", "",
|
||||||
eventStore)
|
eventStore, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed creating a manager: %v", err)
|
log.Fatalf("failed creating a manager: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ func (w *Worker) generateProperties() properties {
|
|||||||
rulesDirection map[string]int
|
rulesDirection map[string]int
|
||||||
groups int
|
groups int
|
||||||
routes int
|
routes int
|
||||||
|
routesWithRGGroups int
|
||||||
nameservers int
|
nameservers int
|
||||||
uiClient int
|
uiClient int
|
||||||
version string
|
version string
|
||||||
@@ -201,6 +202,11 @@ func (w *Worker) generateProperties() properties {
|
|||||||
|
|
||||||
groups = groups + len(account.Groups)
|
groups = groups + len(account.Groups)
|
||||||
routes = routes + len(account.Routes)
|
routes = routes + len(account.Routes)
|
||||||
|
for _, route := range account.Routes {
|
||||||
|
if len(route.PeerGroups) > 0 {
|
||||||
|
routesWithRGGroups++
|
||||||
|
}
|
||||||
|
}
|
||||||
nameservers = nameservers + len(account.NameServerGroups)
|
nameservers = nameservers + len(account.NameServerGroups)
|
||||||
|
|
||||||
for _, policy := range account.Policies {
|
for _, policy := range account.Policies {
|
||||||
@@ -282,6 +288,7 @@ func (w *Worker) generateProperties() properties {
|
|||||||
metricsProperties["rules"] = rules
|
metricsProperties["rules"] = rules
|
||||||
metricsProperties["groups"] = groups
|
metricsProperties["groups"] = groups
|
||||||
metricsProperties["routes"] = routes
|
metricsProperties["routes"] = routes
|
||||||
|
metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
|
||||||
metricsProperties["nameservers"] = nameservers
|
metricsProperties["nameservers"] = nameservers
|
||||||
metricsProperties["version"] = version
|
metricsProperties["version"] = version
|
||||||
metricsProperties["min_active_peer_version"] = minActivePeerVersion
|
metricsProperties["min_active_peer_version"] = minActivePeerVersion
|
||||||
|
|||||||
239
management/server/metrics/selfhosted_test.go
Normal file
239
management/server/metrics/selfhosted_test.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockDatasource struct{}
|
||||||
|
|
||||||
|
// GetAllConnectedPeers returns a map of connected peer IDs for use in tests with predefined information
|
||||||
|
func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
|
||||||
|
return map[string]struct{}{
|
||||||
|
"1": {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAccounts returns a list of *server.Account for use in tests with predefined information
|
||||||
|
func (mockDatasource) GetAllAccounts() []*server.Account {
|
||||||
|
return []*server.Account{
|
||||||
|
{
|
||||||
|
Id: "1",
|
||||||
|
Settings: &server.Settings{PeerLoginExpirationEnabled: true},
|
||||||
|
SetupKeys: map[string]*server.SetupKey{
|
||||||
|
"1": {
|
||||||
|
Id: "1",
|
||||||
|
Ephemeral: true,
|
||||||
|
UsedTimes: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: map[string]*server.Group{
|
||||||
|
"1": {},
|
||||||
|
"2": {},
|
||||||
|
},
|
||||||
|
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
Peers: map[string]*server.Peer{
|
||||||
|
"1": {
|
||||||
|
ID: "1",
|
||||||
|
UserID: "test",
|
||||||
|
SSHEnabled: true,
|
||||||
|
Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policies: []*server.Policy{
|
||||||
|
{
|
||||||
|
Rules: []*server.PolicyRule{
|
||||||
|
{
|
||||||
|
Bidirectional: true,
|
||||||
|
Protocol: server.PolicyRuleProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rules: []*server.PolicyRule{
|
||||||
|
{
|
||||||
|
Bidirectional: false,
|
||||||
|
Protocol: server.PolicyRuleProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: map[string]*route.Route{
|
||||||
|
"1": {
|
||||||
|
ID: "1",
|
||||||
|
PeerGroups: make([]string, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Users: map[string]*server.User{
|
||||||
|
"1": {
|
||||||
|
IsServiceUser: true,
|
||||||
|
PATs: map[string]*server.PersonalAccessToken{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
IsServiceUser: false,
|
||||||
|
PATs: map[string]*server.PersonalAccessToken{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: "2",
|
||||||
|
Settings: &server.Settings{PeerLoginExpirationEnabled: true},
|
||||||
|
SetupKeys: map[string]*server.SetupKey{
|
||||||
|
"1": {
|
||||||
|
Id: "1",
|
||||||
|
Ephemeral: true,
|
||||||
|
UsedTimes: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Groups: map[string]*server.Group{
|
||||||
|
"1": {},
|
||||||
|
"2": {},
|
||||||
|
},
|
||||||
|
NameServerGroups: map[string]*nbdns.NameServerGroup{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
Peers: map[string]*server.Peer{
|
||||||
|
"1": {
|
||||||
|
ID: "1",
|
||||||
|
UserID: "test",
|
||||||
|
SSHEnabled: true,
|
||||||
|
Meta: server.PeerSystemMeta{GoOS: "linux", WtVersion: "0.0.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Policies: []*server.Policy{
|
||||||
|
{
|
||||||
|
Rules: []*server.PolicyRule{
|
||||||
|
{
|
||||||
|
Bidirectional: true,
|
||||||
|
Protocol: server.PolicyRuleProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Rules: []*server.PolicyRule{
|
||||||
|
{
|
||||||
|
Bidirectional: false,
|
||||||
|
Protocol: server.PolicyRuleProtocolTCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: map[string]*route.Route{
|
||||||
|
"1": {
|
||||||
|
ID: "1",
|
||||||
|
PeerGroups: make([]string, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Users: map[string]*server.User{
|
||||||
|
"1": {
|
||||||
|
IsServiceUser: true,
|
||||||
|
PATs: map[string]*server.PersonalAccessToken{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
IsServiceUser: false,
|
||||||
|
PATs: map[string]*server.PersonalAccessToken{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties
|
||||||
|
func TestGenerateProperties(t *testing.T) {
|
||||||
|
ds := mockDatasource{}
|
||||||
|
worker := Worker{
|
||||||
|
dataSource: ds,
|
||||||
|
connManager: ds,
|
||||||
|
}
|
||||||
|
|
||||||
|
properties := worker.generateProperties()
|
||||||
|
|
||||||
|
if properties["accounts"] != 2 {
|
||||||
|
t.Errorf("expected 2 accounts, got %d", properties["accounts"])
|
||||||
|
}
|
||||||
|
if properties["peers"] != 2 {
|
||||||
|
t.Errorf("expected 2 peers, got %d", properties["peers"])
|
||||||
|
}
|
||||||
|
if properties["routes"] != 2 {
|
||||||
|
t.Errorf("expected 2 routes, got %d", properties["routes"])
|
||||||
|
}
|
||||||
|
if properties["rules"] != 4 {
|
||||||
|
t.Errorf("expected 4 rules, got %d", properties["rules"])
|
||||||
|
}
|
||||||
|
if properties["users"] != 2 {
|
||||||
|
t.Errorf("expected 1 users, got %d", properties["users"])
|
||||||
|
}
|
||||||
|
if properties["setup_keys_usage"] != 2 {
|
||||||
|
t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
|
||||||
|
}
|
||||||
|
if properties["pats"] != 4 {
|
||||||
|
t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
|
||||||
|
}
|
||||||
|
if properties["peers_ssh_enabled"] != 2 {
|
||||||
|
t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
|
||||||
|
}
|
||||||
|
if properties["routes_with_routing_groups"] != 2 {
|
||||||
|
t.Errorf("expected 2 routes_with_routing_groups, got %d", properties["routes_with_routing_groups"])
|
||||||
|
}
|
||||||
|
if properties["rules_protocol_tcp"] != 4 {
|
||||||
|
t.Errorf("expected 4 rules_protocol_tcp, got %d", properties["rules_protocol_tcp"])
|
||||||
|
}
|
||||||
|
if properties["rules_direction_oneway"] != 2 {
|
||||||
|
t.Errorf("expected 2 rules_direction_oneway, got %d", properties["rules_direction_oneway"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["active_peers_last_day"] != 2 {
|
||||||
|
t.Errorf("expected 2 active_peers_last_day, got %d", properties["active_peers_last_day"])
|
||||||
|
}
|
||||||
|
if properties["min_active_peer_version"] != "0.0.1" {
|
||||||
|
t.Errorf("expected 0.0.1 min_active_peer_version, got %s", properties["min_active_peer_version"])
|
||||||
|
}
|
||||||
|
if properties["max_active_peer_version"] != "0.0.1" {
|
||||||
|
t.Errorf("expected 0.0.1 max_active_peer_version, got %s", properties["max_active_peer_version"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["peers_login_expiration_enabled"] != 2 {
|
||||||
|
t.Errorf("expected 2 peers_login_expiration_enabled, got %d", properties["peers_login_expiration_enabled"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["service_users"] != 2 {
|
||||||
|
t.Errorf("expected 2 service_users, got %d", properties["service_users"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["peer_os_linux"] != 2 {
|
||||||
|
t.Errorf("expected 2 peer_os_linux, got %d", properties["peer_os_linux"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["ephemeral_peers_setup_keys"] != 2 {
|
||||||
|
t.Errorf("expected 2 ephemeral_peers_setup_keys, got %d", properties["ephemeral_peers_setup_keys_usage"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["ephemeral_peers_setup_keys_usage"] != 2 {
|
||||||
|
t.Errorf("expected 2 ephemeral_peers_setup_keys_usage, got %d", properties["ephemeral_peers_setup_keys_usage"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["nameservers"] != 2 {
|
||||||
|
t.Errorf("expected 2 nameservers, got %d", properties["nameservers"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["groups"] != 4 {
|
||||||
|
t.Errorf("expected 4 groups, got %d", properties["groups"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if properties["user_peers"] != 2 {
|
||||||
|
t.Errorf("expected 2 user_peers, got %d", properties["user_peers"])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,23 +20,18 @@ type MockAccountManager struct {
|
|||||||
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
|
GetSetupKeyFunc func(accountID, userID, keyID string) (*server.SetupKey, error)
|
||||||
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
|
GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error)
|
||||||
GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error)
|
GetUserFunc func(claims jwtclaims.AuthorizationClaims) (*server.User, error)
|
||||||
AccountExistsFunc func(accountId string) (*bool, error)
|
|
||||||
GetPeerByKeyFunc func(peerKey string) (*server.Peer, error)
|
|
||||||
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
|
GetPeersFunc func(accountID, userID string) ([]*server.Peer, error)
|
||||||
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
MarkPeerConnectedFunc func(peerKey string, connected bool) error
|
||||||
DeletePeerFunc func(accountID, peerKey, userID string) (*server.Peer, error)
|
DeletePeerFunc func(accountID, peerKey, userID string) error
|
||||||
GetPeerByIPFunc func(accountId string, peerIP string) (*server.Peer, error)
|
|
||||||
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
GetNetworkMapFunc func(peerKey string) (*server.NetworkMap, error)
|
||||||
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
GetPeerNetworkFunc func(peerKey string) (*server.Network, error)
|
||||||
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, *server.NetworkMap, error)
|
AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, *server.NetworkMap, error)
|
||||||
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
|
GetGroupFunc func(accountID, groupID string) (*server.Group, error)
|
||||||
SaveGroupFunc func(accountID, userID string, group *server.Group) error
|
SaveGroupFunc func(accountID, userID string, group *server.Group) error
|
||||||
UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error)
|
|
||||||
DeleteGroupFunc func(accountID, userId, groupID string) error
|
DeleteGroupFunc func(accountID, userId, groupID string) error
|
||||||
ListGroupsFunc func(accountID string) ([]*server.Group, error)
|
ListGroupsFunc func(accountID string) ([]*server.Group, error)
|
||||||
GroupAddPeerFunc func(accountID, groupID, peerKey string) error
|
GroupAddPeerFunc func(accountID, groupID, peerID string) error
|
||||||
GroupDeletePeerFunc func(accountID, groupID, peerKey string) error
|
GroupDeletePeerFunc func(accountID, groupID, peerID string) error
|
||||||
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
|
|
||||||
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
|
GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error)
|
||||||
SaveRuleFunc func(accountID, userID string, rule *server.Rule) error
|
SaveRuleFunc func(accountID, userID string, rule *server.Rule) error
|
||||||
DeleteRuleFunc func(accountID, ruleID, userID string) error
|
DeleteRuleFunc func(accountID, ruleID, userID string) error
|
||||||
@@ -51,10 +46,9 @@ type MockAccountManager struct {
|
|||||||
UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error
|
UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error
|
||||||
UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error
|
UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error
|
||||||
UpdatePeerFunc func(accountID, userID string, peer *server.Peer) (*server.Peer, error)
|
UpdatePeerFunc func(accountID, userID string, peer *server.Peer) (*server.Peer, error)
|
||||||
CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error)
|
CreateRouteFunc func(accountID, prefix, peer string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error)
|
||||||
GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error)
|
GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error)
|
||||||
SaveRouteFunc func(accountID, userID string, route *route.Route) error
|
SaveRouteFunc func(accountID, userID string, route *route.Route) error
|
||||||
UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error)
|
|
||||||
DeleteRouteFunc func(accountID, routeID, userID string) error
|
DeleteRouteFunc func(accountID, routeID, userID string) error
|
||||||
ListRoutesFunc func(accountID, userID string) ([]*route.Route, error)
|
ListRoutesFunc func(accountID, userID string) ([]*route.Route, error)
|
||||||
SaveSetupKeyFunc func(accountID string, key *server.SetupKey, userID string) (*server.SetupKey, error)
|
SaveSetupKeyFunc func(accountID string, key *server.SetupKey, userID string) (*server.SetupKey, error)
|
||||||
@@ -68,7 +62,6 @@ type MockAccountManager struct {
|
|||||||
GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error)
|
||||||
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error)
|
CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error)
|
||||||
SaveNameServerGroupFunc func(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
|
SaveNameServerGroupFunc func(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error
|
||||||
UpdateNameServerGroupFunc func(accountID, nsGroupID, userID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error)
|
|
||||||
DeleteNameServerGroupFunc func(accountID, nsGroupID, userID string) error
|
DeleteNameServerGroupFunc func(accountID, nsGroupID, userID string) error
|
||||||
ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error)
|
ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error)
|
||||||
CreateUserFunc func(accountID, userID string, key *server.UserInfo) (*server.UserInfo, error)
|
CreateUserFunc func(accountID, userID string, key *server.UserInfo) (*server.UserInfo, error)
|
||||||
@@ -82,6 +75,7 @@ type MockAccountManager struct {
|
|||||||
LoginPeerFunc func(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error)
|
LoginPeerFunc func(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error)
|
||||||
SyncPeerFunc func(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error)
|
SyncPeerFunc func(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error)
|
||||||
InviteUserFunc func(accountID string, initiatorUserID string, targetUserEmail string) error
|
InviteUserFunc func(accountID string, initiatorUserID string, targetUserEmail string) error
|
||||||
|
GetAllConnectedPeersFunc func() (map[string]struct{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
|
// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface
|
||||||
@@ -93,11 +87,11 @@ func (am *MockAccountManager) GetUsersFromAccount(accountID string, userID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
|
// DeletePeer mock implementation of DeletePeer from server.AccountManager interface
|
||||||
func (am *MockAccountManager) DeletePeer(accountID, peerID, userID string) (*server.Peer, error) {
|
func (am *MockAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||||
if am.DeletePeerFunc != nil {
|
if am.DeletePeerFunc != nil {
|
||||||
return am.DeletePeerFunc(accountID, peerID, userID)
|
return am.DeletePeerFunc(accountID, peerID, userID)
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
|
return status.Errorf(codes.Unimplemented, "method DeletePeer is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface
|
// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface
|
||||||
@@ -143,22 +137,6 @@ func (am *MockAccountManager) GetAccountByUserOrAccountID(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountExists mock implementation of AccountExists from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
|
|
||||||
if am.AccountExistsFunc != nil {
|
|
||||||
return am.AccountExistsFunc(accountId)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method AccountExists is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerByKey mocks implementation of GetPeerByKey from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) GetPeerByKey(peerKey string) (*server.Peer, error) {
|
|
||||||
if am.GetPeerByKeyFunc != nil {
|
|
||||||
return am.GetPeerByKeyFunc(peerKey)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeerByKey is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkPeerConnected mock implementation of MarkPeerConnected from server.AccountManager interface
|
// 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) error {
|
||||||
if am.MarkPeerConnectedFunc != nil {
|
if am.MarkPeerConnectedFunc != nil {
|
||||||
@@ -167,14 +145,6 @@ func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool)
|
|||||||
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
|
return status.Errorf(codes.Unimplemented, "method MarkPeerConnected is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerByIP mock implementation of GetPeerByIP from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
|
|
||||||
if am.GetPeerByIPFunc != nil {
|
|
||||||
return am.GetPeerByIPFunc(accountId, peerIP)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeerByIP is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAccountFromPAT mock implementation of GetAccountFromPAT from server.AccountManager interface
|
// GetAccountFromPAT mock implementation of GetAccountFromPAT from server.AccountManager interface
|
||||||
func (am *MockAccountManager) GetAccountFromPAT(pat string) (*server.Account, *server.User, *server.PersonalAccessToken, error) {
|
func (am *MockAccountManager) GetAccountFromPAT(pat string) (*server.Account, *server.User, *server.PersonalAccessToken, error) {
|
||||||
if am.GetAccountFromPATFunc != nil {
|
if am.GetAccountFromPATFunc != nil {
|
||||||
@@ -267,14 +237,6 @@ func (am *MockAccountManager) SaveGroup(accountID, userID string, group *server.
|
|||||||
return status.Errorf(codes.Unimplemented, "method SaveGroup is not implemented")
|
return status.Errorf(codes.Unimplemented, "method SaveGroup is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGroup mock implementation of UpdateGroup from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) UpdateGroup(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) {
|
|
||||||
if am.UpdateGroupFunc != nil {
|
|
||||||
return am.UpdateGroupFunc(accountID, groupID, operations)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateGroup not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteGroup mock implementation of DeleteGroup from server.AccountManager interface
|
// DeleteGroup mock implementation of DeleteGroup from server.AccountManager interface
|
||||||
func (am *MockAccountManager) DeleteGroup(accountId, userId, groupID string) error {
|
func (am *MockAccountManager) DeleteGroup(accountId, userId, groupID string) error {
|
||||||
if am.DeleteGroupFunc != nil {
|
if am.DeleteGroupFunc != nil {
|
||||||
@@ -292,29 +254,21 @@ func (am *MockAccountManager) ListGroups(accountID string) ([]*server.Group, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GroupAddPeer mock implementation of GroupAddPeer from server.AccountManager interface
|
// GroupAddPeer mock implementation of GroupAddPeer from server.AccountManager interface
|
||||||
func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerKey string) error {
|
func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerID string) error {
|
||||||
if am.GroupAddPeerFunc != nil {
|
if am.GroupAddPeerFunc != nil {
|
||||||
return am.GroupAddPeerFunc(accountID, groupID, peerKey)
|
return am.GroupAddPeerFunc(accountID, groupID, peerID)
|
||||||
}
|
}
|
||||||
return status.Errorf(codes.Unimplemented, "method GroupAddPeer is not implemented")
|
return status.Errorf(codes.Unimplemented, "method GroupAddPeer is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupDeletePeer mock implementation of GroupDeletePeer from server.AccountManager interface
|
// GroupDeletePeer mock implementation of GroupDeletePeer from server.AccountManager interface
|
||||||
func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error {
|
func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error {
|
||||||
if am.GroupDeletePeerFunc != nil {
|
if am.GroupDeletePeerFunc != nil {
|
||||||
return am.GroupDeletePeerFunc(accountID, groupID, peerKey)
|
return am.GroupDeletePeerFunc(accountID, groupID, peerID)
|
||||||
}
|
}
|
||||||
return status.Errorf(codes.Unimplemented, "method GroupDeletePeer is not implemented")
|
return status.Errorf(codes.Unimplemented, "method GroupDeletePeer is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupListPeers mock implementation of GroupListPeers from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*server.Peer, error) {
|
|
||||||
if am.GroupListPeersFunc != nil {
|
|
||||||
return am.GroupListPeersFunc(accountID, groupID)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GroupListPeers is not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRule mock implementation of GetRule from server.AccountManager interface
|
// GetRule mock implementation of GetRule from server.AccountManager interface
|
||||||
func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server.Rule, error) {
|
func (am *MockAccountManager) GetRule(accountID, ruleID, userID string) (*server.Rule, error) {
|
||||||
if am.GetRuleFunc != nil {
|
if am.GetRuleFunc != nil {
|
||||||
@@ -412,9 +366,9 @@ func (am *MockAccountManager) UpdatePeer(accountID, userID string, peer *server.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateRoute mock implementation of CreateRoute from server.AccountManager interface
|
// CreateRoute mock implementation of CreateRoute from server.AccountManager interface
|
||||||
func (am *MockAccountManager) CreateRoute(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) {
|
func (am *MockAccountManager) CreateRoute(accountID, network, peerID string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) {
|
||||||
if am.CreateRouteFunc != nil {
|
if am.CreateRouteFunc != nil {
|
||||||
return am.CreateRouteFunc(accountID, network, peerID, description, netID, masquerade, metric, groups, enabled, userID)
|
return am.CreateRouteFunc(accountID, network, peerID, peerGroups, description, netID, masquerade, metric, groups, enabled, userID)
|
||||||
}
|
}
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented")
|
||||||
}
|
}
|
||||||
@@ -435,14 +389,6 @@ func (am *MockAccountManager) SaveRoute(accountID, userID string, route *route.R
|
|||||||
return status.Errorf(codes.Unimplemented, "method SaveRoute is not implemented")
|
return status.Errorf(codes.Unimplemented, "method SaveRoute is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRoute mock implementation of UpdateRoute from server.AccountManager interface
|
|
||||||
func (am *MockAccountManager) UpdateRoute(accountID, ruleID string, operations []server.RouteUpdateOperation) (*route.Route, error) {
|
|
||||||
if am.UpdateRouteFunc != nil {
|
|
||||||
return am.UpdateRouteFunc(accountID, ruleID, operations)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoute not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRoute mock implementation of DeleteRoute from server.AccountManager interface
|
// DeleteRoute mock implementation of DeleteRoute from server.AccountManager interface
|
||||||
func (am *MockAccountManager) DeleteRoute(accountID, routeID, userID string) error {
|
func (am *MockAccountManager) DeleteRoute(accountID, routeID, userID string) error {
|
||||||
if am.DeleteRouteFunc != nil {
|
if am.DeleteRouteFunc != nil {
|
||||||
@@ -533,14 +479,6 @@ func (am *MockAccountManager) SaveNameServerGroup(accountID, userID string, nsGr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateNameServerGroup mocks UpdateNameServerGroup of the AccountManager interface
|
|
||||||
func (am *MockAccountManager) UpdateNameServerGroup(accountID, nsGroupID, userID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
|
||||||
if am.UpdateNameServerGroupFunc != nil {
|
|
||||||
return am.UpdateNameServerGroupFunc(accountID, nsGroupID, userID, operations)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteNameServerGroup mocks DeleteNameServerGroup of the AccountManager interface
|
// DeleteNameServerGroup mocks DeleteNameServerGroup of the AccountManager interface
|
||||||
func (am *MockAccountManager) DeleteNameServerGroup(accountID, nsGroupID, userID string) error {
|
func (am *MockAccountManager) DeleteNameServerGroup(accountID, nsGroupID, userID string) error {
|
||||||
if am.DeleteNameServerGroupFunc != nil {
|
if am.DeleteNameServerGroupFunc != nil {
|
||||||
@@ -646,3 +584,11 @@ func (am *MockAccountManager) SyncPeer(sync server.PeerSync) (*server.Peer, *ser
|
|||||||
}
|
}
|
||||||
return nil, nil, status.Errorf(codes.Unimplemented, "method SyncPeer is not implemented")
|
return nil, nil, status.Errorf(codes.Unimplemented, "method SyncPeer is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllConnectedPeers mocks GetAllConnectedPeers of the AccountManager interface
|
||||||
|
func (am *MockAccountManager) GetAllConnectedPeers() (map[string]struct{}, error) {
|
||||||
|
if am.GetAllConnectedPeersFunc != nil {
|
||||||
|
return am.GetAllConnectedPeersFunc()
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetAllConnectedPeers is not implemented")
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,66 +3,17 @@ package server
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
"github.com/netbirdio/netbird/management/server/status"
|
"github.com/netbirdio/netbird/management/server/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$`
|
||||||
// UpdateNameServerGroupName indicates a nameserver group name update operation
|
|
||||||
UpdateNameServerGroupName NameServerGroupUpdateOperationType = iota
|
|
||||||
// UpdateNameServerGroupDescription indicates a nameserver group description update operation
|
|
||||||
UpdateNameServerGroupDescription
|
|
||||||
// UpdateNameServerGroupNameServers indicates a nameserver group nameservers list update operation
|
|
||||||
UpdateNameServerGroupNameServers
|
|
||||||
// UpdateNameServerGroupGroups indicates a nameserver group' groups update operation
|
|
||||||
UpdateNameServerGroupGroups
|
|
||||||
// UpdateNameServerGroupEnabled indicates a nameserver group status update operation
|
|
||||||
UpdateNameServerGroupEnabled
|
|
||||||
// UpdateNameServerGroupPrimary indicates a nameserver group primary status update operation
|
|
||||||
UpdateNameServerGroupPrimary
|
|
||||||
// UpdateNameServerGroupDomains indicates a nameserver group' domains update operation
|
|
||||||
UpdateNameServerGroupDomains
|
|
||||||
|
|
||||||
domainPattern = `^(?i)[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$`
|
|
||||||
)
|
|
||||||
|
|
||||||
// NameServerGroupUpdateOperationType operation type
|
|
||||||
type NameServerGroupUpdateOperationType int
|
|
||||||
|
|
||||||
func (t NameServerGroupUpdateOperationType) String() string {
|
|
||||||
switch t {
|
|
||||||
case UpdateNameServerGroupDescription:
|
|
||||||
return "UpdateNameServerGroupDescription"
|
|
||||||
case UpdateNameServerGroupName:
|
|
||||||
return "UpdateNameServerGroupName"
|
|
||||||
case UpdateNameServerGroupNameServers:
|
|
||||||
return "UpdateNameServerGroupNameServers"
|
|
||||||
case UpdateNameServerGroupGroups:
|
|
||||||
return "UpdateNameServerGroupGroups"
|
|
||||||
case UpdateNameServerGroupEnabled:
|
|
||||||
return "UpdateNameServerGroupEnabled"
|
|
||||||
case UpdateNameServerGroupPrimary:
|
|
||||||
return "UpdateNameServerGroupPrimary"
|
|
||||||
case UpdateNameServerGroupDomains:
|
|
||||||
return "UpdateNameServerGroupDomains"
|
|
||||||
default:
|
|
||||||
return "InvalidOperation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NameServerGroupUpdateOperation operation object with type and values to be applied
|
|
||||||
type NameServerGroupUpdateOperation struct {
|
|
||||||
Type NameServerGroupUpdateOperationType
|
|
||||||
Values []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
|
// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs
|
||||||
func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) {
|
||||||
@@ -122,11 +73,7 @@ func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, d
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return newNSGroup.Copy(), status.Errorf(status.Internal, "failed to update peers after create nameserver %s", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
am.storeEvent(userID, newNSGroup.ID, accountID, activity.NameserverGroupCreated, newNSGroup.EventMeta())
|
am.storeEvent(userID, newNSGroup.ID, accountID, activity.NameserverGroupCreated, newNSGroup.EventMeta())
|
||||||
|
|
||||||
@@ -161,120 +108,13 @@ func (am *DefaultAccountManager) SaveNameServerGroup(accountID, userID string, n
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return status.Errorf(status.Internal, "failed to update peers after update nameserver %s", nsGroupToSave.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
am.storeEvent(userID, nsGroupToSave.ID, accountID, activity.NameserverGroupUpdated, nsGroupToSave.EventMeta())
|
am.storeEvent(userID, nsGroupToSave.ID, accountID, activity.NameserverGroupUpdated, nsGroupToSave.EventMeta())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateNameServerGroup updates existing nameserver group with set of operations
|
|
||||||
func (am *DefaultAccountManager) UpdateNameServerGroup(accountID, nsGroupID, userID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) {
|
|
||||||
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(operations) == 0 {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "operations shouldn't be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
nsGroupToUpdate, ok := account.NameServerGroups[nsGroupID]
|
|
||||||
if !ok {
|
|
||||||
return nil, status.Errorf(status.NotFound, "nameserver group ID %s no longer exists", nsGroupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
newNSGroup := nsGroupToUpdate.Copy()
|
|
||||||
|
|
||||||
for _, operation := range operations {
|
|
||||||
valuesCount := len(operation.Values)
|
|
||||||
if valuesCount < 1 {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "operation %s contains invalid number of values, it should be at least 1", operation.Type.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range operation.Values {
|
|
||||||
if value == "" {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "operation %s contains invalid empty string value", operation.Type.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch operation.Type {
|
|
||||||
case UpdateNameServerGroupDescription:
|
|
||||||
newNSGroup.Description = operation.Values[0]
|
|
||||||
case UpdateNameServerGroupName:
|
|
||||||
if valuesCount > 1 {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse name values, expected 1 value got %d", valuesCount)
|
|
||||||
}
|
|
||||||
err = validateNSGroupName(operation.Values[0], nsGroupID, account.NameServerGroups)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newNSGroup.Name = operation.Values[0]
|
|
||||||
case UpdateNameServerGroupNameServers:
|
|
||||||
var nsList []nbdns.NameServer
|
|
||||||
for _, url := range operation.Values {
|
|
||||||
ns, err := nbdns.ParseNameServerURL(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
nsList = append(nsList, ns)
|
|
||||||
}
|
|
||||||
err = validateNSList(nsList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newNSGroup.NameServers = nsList
|
|
||||||
case UpdateNameServerGroupGroups:
|
|
||||||
err = validateGroups(operation.Values, account.Groups)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newNSGroup.Groups = operation.Values
|
|
||||||
case UpdateNameServerGroupEnabled:
|
|
||||||
enabled, err := strconv.ParseBool(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0])
|
|
||||||
}
|
|
||||||
newNSGroup.Enabled = enabled
|
|
||||||
case UpdateNameServerGroupPrimary:
|
|
||||||
primary, err := strconv.ParseBool(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse primary status %s, not boolean", operation.Values[0])
|
|
||||||
}
|
|
||||||
newNSGroup.Primary = primary
|
|
||||||
case UpdateNameServerGroupDomains:
|
|
||||||
err = validateDomainInput(false, operation.Values)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newNSGroup.Domains = operation.Values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
account.NameServerGroups[nsGroupID] = newNSGroup
|
|
||||||
|
|
||||||
account.Network.IncSerial()
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return newNSGroup.Copy(), status.Errorf(status.Internal, "failed to update peers after update nameserver %s", newNSGroup.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newNSGroup.Copy(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteNameServerGroup deletes nameserver group with nsGroupID
|
// DeleteNameServerGroup deletes nameserver group with nsGroupID
|
||||||
func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID, userID string) error {
|
func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID, userID string) error {
|
||||||
|
|
||||||
@@ -298,10 +138,7 @@ func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return status.Errorf(status.Internal, "failed to update peers after deleting nameserver %s", nsGroupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
am.storeEvent(userID, nsGroup.ID, accountID, activity.NameserverGroupDeleted, nsGroup.EventMeta())
|
am.storeEvent(userID, nsGroup.ID, accountID, activity.NameserverGroupDeleted, nsGroup.EventMeta())
|
||||||
|
|
||||||
|
|||||||
@@ -655,323 +655,6 @@ func TestSaveNameServerGroup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateNameServerGroup(t *testing.T) {
|
|
||||||
nsGroupID := "testingNSGroup"
|
|
||||||
|
|
||||||
existingNSGroup := &nbdns.NameServerGroup{
|
|
||||||
ID: nsGroupID,
|
|
||||||
Name: "super",
|
|
||||||
Description: "super",
|
|
||||||
Primary: true,
|
|
||||||
NameServers: []nbdns.NameServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("1.1.1.1"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("1.1.2.2"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Groups: []string{group1ID},
|
|
||||||
Enabled: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
existingNSGroup *nbdns.NameServerGroup
|
|
||||||
nsGroupID string
|
|
||||||
operations []NameServerGroupUpdateOperation
|
|
||||||
shouldCreate bool
|
|
||||||
errFunc require.ErrorAssertionFunc
|
|
||||||
expectedNSGroup *nbdns.NameServerGroup
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Should Config Single Property",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{"superNew"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.NoError,
|
|
||||||
shouldCreate: true,
|
|
||||||
expectedNSGroup: &nbdns.NameServerGroup{
|
|
||||||
ID: nsGroupID,
|
|
||||||
Name: "superNew",
|
|
||||||
Description: "super",
|
|
||||||
Primary: true,
|
|
||||||
NameServers: []nbdns.NameServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("1.1.1.1"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("1.1.2.2"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Groups: []string{group1ID},
|
|
||||||
Enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Config Multiple Properties",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{"superNew"},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupDescription,
|
|
||||||
Values: []string{"superDescription"},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupNameServers,
|
|
||||||
Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53"},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupGroups,
|
|
||||||
Values: []string{group1ID, group2ID},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupEnabled,
|
|
||||||
Values: []string{"false"},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupPrimary,
|
|
||||||
Values: []string{"false"},
|
|
||||||
},
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupDomains,
|
|
||||||
Values: []string{validDomain},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.NoError,
|
|
||||||
shouldCreate: true,
|
|
||||||
expectedNSGroup: &nbdns.NameServerGroup{
|
|
||||||
ID: nsGroupID,
|
|
||||||
Name: "superNew",
|
|
||||||
Description: "superDescription",
|
|
||||||
Primary: false,
|
|
||||||
Domains: []string{validDomain},
|
|
||||||
NameServers: []nbdns.NameServer{
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("127.0.0.1"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
IP: netip.MustParseAddr("8.8.8.8"),
|
|
||||||
NSType: nbdns.UDPNameServerType,
|
|
||||||
Port: nbdns.DefaultDNSPort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Groups: []string{group1ID, group2ID},
|
|
||||||
Enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid ID",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: "nonExistingNSGroup",
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Empty Operations",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Empty Values",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Empty String",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{""},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Name Large String",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid On Existing Name",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{existingNSGroupName},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid On Multiple Name Values",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupName,
|
|
||||||
Values: []string{"nameOne", "nameTwo"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Boolean",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupEnabled,
|
|
||||||
Values: []string{"yes"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Nameservers Wrong Schema",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupNameServers,
|
|
||||||
Values: []string{"https://127.0.0.1:53"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Nameservers Wrong IP",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupNameServers,
|
|
||||||
Values: []string{"udp://8.8.8.300:53"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Large Number Of Nameservers",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupNameServers,
|
|
||||||
Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53", "udp://8.8.4.4:53"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid GroupID",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupGroups,
|
|
||||||
Values: []string{"nonExistingGroupID"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Domains",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupDomains,
|
|
||||||
Values: []string{invalidDomain},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Config On Invalid Primary Status",
|
|
||||||
existingNSGroup: existingNSGroup,
|
|
||||||
nsGroupID: existingNSGroup.ID,
|
|
||||||
operations: []NameServerGroupUpdateOperation{
|
|
||||||
NameServerGroupUpdateOperation{
|
|
||||||
Type: UpdateNameServerGroupPrimary,
|
|
||||||
Values: []string{"yes"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
am, err := createNSManager(t)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("failed to create account manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := initTestNSAccount(t, am)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("failed to init testing account")
|
|
||||||
}
|
|
||||||
|
|
||||||
account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup
|
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("account should be saved")
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedRoute, err := am.UpdateNameServerGroup(account.Id, testCase.nsGroupID, userID, testCase.operations)
|
|
||||||
testCase.errFunc(t, err)
|
|
||||||
|
|
||||||
if !testCase.shouldCreate {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase.expectedNSGroup.ID = updatedRoute.ID
|
|
||||||
|
|
||||||
if !testCase.expectedNSGroup.IsEqual(updatedRoute) {
|
|
||||||
t.Errorf("new nameserver group didn't match expected group:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedNSGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteNameServerGroup(t *testing.T) {
|
func TestDeleteNameServerGroup(t *testing.T) {
|
||||||
nsGroupID := "testingNSGroup"
|
nsGroupID := "testingNSGroup"
|
||||||
|
|
||||||
@@ -1061,7 +744,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
|
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNSStore(t *testing.T) (Store, error) {
|
func createNSStore(t *testing.T) (Store, error) {
|
||||||
|
|||||||
@@ -195,16 +195,6 @@ func (p *PeerStatus) Copy() *PeerStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeerByKey looks up peer by its public WireGuard key
|
|
||||||
func (am *DefaultAccountManager) GetPeerByKey(peerPubKey string) (*Peer, error) {
|
|
||||||
account, err := am.Store.GetAccountByPeerPubKey(peerPubKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return account.FindPeerByPubKey(peerPubKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
// GetPeers returns a list of peers under the given account filtering out peers that do not belong to a user if
|
||||||
// the current user is not an admin.
|
// the current user is not an admin.
|
||||||
func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, error) {
|
func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, error) {
|
||||||
@@ -290,10 +280,7 @@ func (am *DefaultAccountManager) MarkPeerConnected(peerPubKey string, connected
|
|||||||
if oldStatus.LoginExpired {
|
if oldStatus.LoginExpired {
|
||||||
// we need to update other peers because when peer login expires all other peers are notified to disconnect from
|
// we need to update other peers because when peer login expires all other peers are notified to disconnect from
|
||||||
// the expired one. Here we notify them that connection is now allowed again.
|
// the expired one. Here we notify them that connection is now allowed again.
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -364,82 +351,75 @@ func (am *DefaultAccountManager) UpdatePeer(accountID, userID string, update *Pe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deletePeers will delete all specified peers and send updates to the remote peers. Don't call without acquiring account lock
|
||||||
|
func (am *DefaultAccountManager) deletePeers(account *Account, peerIDs []string, userID string) error {
|
||||||
|
|
||||||
|
// the first loop is needed to ensure all peers present under the account before modifying, otherwise
|
||||||
|
// we might have some inconsistencies
|
||||||
|
peers := make([]*Peer, 0, len(peerIDs))
|
||||||
|
for _, peerID := range peerIDs {
|
||||||
|
|
||||||
|
peer := account.GetPeer(peerID)
|
||||||
|
if peer == nil {
|
||||||
|
return status.Errorf(status.NotFound, "peer %s not found", peerID)
|
||||||
|
}
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the 2nd loop performs the actual modification
|
||||||
|
for _, peer := range peers {
|
||||||
|
account.DeletePeer(peer.ID)
|
||||||
|
am.peersUpdateManager.SendUpdate(peer.ID,
|
||||||
|
&UpdateMessage{
|
||||||
|
Update: &proto.SyncResponse{
|
||||||
|
// fill those field for backward compatibility
|
||||||
|
RemotePeers: []*proto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: true,
|
||||||
|
// new field
|
||||||
|
NetworkMap: &proto.NetworkMap{
|
||||||
|
Serial: account.Network.CurrentSerial(),
|
||||||
|
RemotePeers: []*proto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: true,
|
||||||
|
FirewallRules: []*proto.FirewallRule{},
|
||||||
|
FirewallRulesIsEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
am.peersUpdateManager.CloseChannel(peer.ID)
|
||||||
|
am.storeEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeletePeer removes peer from the account by its IP
|
// DeletePeer removes peer from the account by its IP
|
||||||
func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) (*Peer, error) {
|
func (am *DefaultAccountManager) DeletePeer(accountID, peerID, userID string) error {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
account, err := am.Store.GetAccount(accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
peer := account.GetPeer(peerID)
|
err = am.deletePeers(account, []string{peerID}, userID)
|
||||||
if peer == nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(status.NotFound, "peer %s not found", peerID)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
account.DeletePeer(peerID)
|
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
err = am.Store.SaveAccount(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.peersUpdateManager.SendUpdate(peer.ID,
|
am.updateAccountPeers(account)
|
||||||
&UpdateMessage{
|
|
||||||
Update: &proto.SyncResponse{
|
|
||||||
// fill those field for backward compatibility
|
|
||||||
RemotePeers: []*proto.RemotePeerConfig{},
|
|
||||||
RemotePeersIsEmpty: true,
|
|
||||||
// new field
|
|
||||||
NetworkMap: &proto.NetworkMap{
|
|
||||||
Serial: account.Network.CurrentSerial(),
|
|
||||||
RemotePeers: []*proto.RemotePeerConfig{},
|
|
||||||
RemotePeersIsEmpty: true,
|
|
||||||
FirewallRules: []*proto.FirewallRule{},
|
|
||||||
FirewallRulesIsEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := am.updateAccountPeers(account); err != nil {
|
return nil
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
am.peersUpdateManager.CloseChannel(peerID)
|
|
||||||
am.storeEvent(userID, peer.ID, account.Id, activity.PeerRemovedByUser, peer.EventMeta(am.GetDNSDomain()))
|
|
||||||
return peer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPeerByIP returns peer by its IP
|
|
||||||
func (am *DefaultAccountManager) GetPeerByIP(accountID string, peerIP string) (*Peer, error) {
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, peer := range account.Peers {
|
|
||||||
if peerIP == peer.IP.String() {
|
|
||||||
return peer, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, status.Errorf(status.NotFound, "peer with IP %s not found", peerIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
// GetNetworkMap returns Network map for a given peer (omits original peer from the Peers result)
|
||||||
@@ -609,10 +589,7 @@ func (am *DefaultAccountManager) AddPeer(setupKey, userID string, peer *Peer) (*
|
|||||||
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
|
opEvent.Meta = newPeer.EventMeta(am.GetDNSDomain())
|
||||||
am.storeEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
am.storeEvent(opEvent.InitiatorID, opEvent.TargetID, opEvent.AccountID, opEvent.Activity, opEvent.Meta)
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
networkMap := account.GetPeerNetworkMap(newPeer.ID, am.dnsDomain)
|
networkMap := account.GetPeerNetworkMap(newPeer.ID, am.dnsDomain)
|
||||||
return newPeer, networkMap, nil
|
return newPeer, networkMap, nil
|
||||||
@@ -727,10 +704,7 @@ func (am *DefaultAccountManager) LoginPeer(login PeerLogin) (*Peer, *NetworkMap,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updateRemotePeers {
|
if updateRemotePeers {
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
return peer, account.GetPeerNetworkMap(peer.ID, am.dnsDomain), nil
|
||||||
}
|
}
|
||||||
@@ -804,10 +778,7 @@ func (am *DefaultAccountManager) checkAndUpdatePeerSSHKey(peer *Peer, account *A
|
|||||||
}
|
}
|
||||||
|
|
||||||
// trigger network map update
|
// trigger network map update
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
@@ -852,7 +823,9 @@ func (am *DefaultAccountManager) UpdatePeerSSHKey(peerID string, sshKey string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// trigger network map update
|
// trigger network map update
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeer for a given accountID, peerID and userID error if not found.
|
// GetPeer for a given accountID, peerID and userID error if not found.
|
||||||
@@ -909,21 +882,12 @@ func updatePeerMeta(peer *Peer, meta PeerSystemMeta, account *Account) (*Peer, b
|
|||||||
|
|
||||||
// updateAccountPeers updates all peers that belong to an account.
|
// updateAccountPeers updates all peers that belong to an account.
|
||||||
// Should be called when changes have to be synced to peers.
|
// Should be called when changes have to be synced to peers.
|
||||||
func (am *DefaultAccountManager) updateAccountPeers(account *Account) error {
|
func (am *DefaultAccountManager) updateAccountPeers(account *Account) {
|
||||||
peers := account.GetPeers()
|
peers := account.GetPeers()
|
||||||
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
remotePeerNetworkMap, err := am.GetNetworkMap(peer.ID)
|
remotePeerNetworkMap := account.GetPeerNetworkMap(peer.ID, am.dnsDomain)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
update := toSyncResponse(nil, peer, nil, remotePeerNetworkMap, am.GetDNSDomain())
|
update := toSyncResponse(nil, peer, nil, remotePeerNetworkMap, am.GetDNSDomain())
|
||||||
err = am.peersUpdateManager.SendUpdate(peer.ID, &UpdateMessage{Update: update})
|
am.peersUpdateManager.SendUpdate(peer.ID, &UpdateMessage{Update: update})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,7 +350,9 @@ func (am *DefaultAccountManager) SavePolicy(accountID, userID string, policy *Po
|
|||||||
}
|
}
|
||||||
am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta())
|
am.storeEvent(userID, policy.ID, accountID, action, policy.EventMeta())
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeletePolicy from the store
|
// DeletePolicy from the store
|
||||||
@@ -375,7 +377,9 @@ func (am *DefaultAccountManager) DeletePolicy(accountID, policyID, userID string
|
|||||||
|
|
||||||
am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta())
|
am.storeEvent(userID, policy.ID, accountID, activity.PolicyRemoved, policy.EventMeta())
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPolicies from the store
|
// ListPolicies from the store
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/proto"
|
"github.com/netbirdio/netbird/management/proto"
|
||||||
@@ -10,60 +9,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/status"
|
"github.com/netbirdio/netbird/management/server/status"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// UpdateRouteDescription indicates a route description update operation
|
|
||||||
UpdateRouteDescription RouteUpdateOperationType = iota
|
|
||||||
// UpdateRouteNetwork indicates a route IP update operation
|
|
||||||
UpdateRouteNetwork
|
|
||||||
// UpdateRoutePeer indicates a route peer update operation
|
|
||||||
UpdateRoutePeer
|
|
||||||
// UpdateRouteMetric indicates a route metric update operation
|
|
||||||
UpdateRouteMetric
|
|
||||||
// UpdateRouteMasquerade indicates a route masquerade update operation
|
|
||||||
UpdateRouteMasquerade
|
|
||||||
// UpdateRouteEnabled indicates a route enabled update operation
|
|
||||||
UpdateRouteEnabled
|
|
||||||
// UpdateRouteNetworkIdentifier indicates a route net ID update operation
|
|
||||||
UpdateRouteNetworkIdentifier
|
|
||||||
// UpdateRouteGroups indicates a group list update operation
|
|
||||||
UpdateRouteGroups
|
|
||||||
)
|
|
||||||
|
|
||||||
// RouteUpdateOperationType operation type
|
|
||||||
type RouteUpdateOperationType int
|
|
||||||
|
|
||||||
func (t RouteUpdateOperationType) String() string {
|
|
||||||
switch t {
|
|
||||||
case UpdateRouteDescription:
|
|
||||||
return "UpdateRouteDescription"
|
|
||||||
case UpdateRouteNetwork:
|
|
||||||
return "UpdateRouteNetwork"
|
|
||||||
case UpdateRoutePeer:
|
|
||||||
return "UpdateRoutePeer"
|
|
||||||
case UpdateRouteMetric:
|
|
||||||
return "UpdateRouteMetric"
|
|
||||||
case UpdateRouteMasquerade:
|
|
||||||
return "UpdateRouteMasquerade"
|
|
||||||
case UpdateRouteEnabled:
|
|
||||||
return "UpdateRouteEnabled"
|
|
||||||
case UpdateRouteNetworkIdentifier:
|
|
||||||
return "UpdateRouteNetworkIdentifier"
|
|
||||||
case UpdateRouteGroups:
|
|
||||||
return "UpdateRouteGroups"
|
|
||||||
default:
|
|
||||||
return "InvalidOperation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouteUpdateOperation operation object with type and values to be applied
|
|
||||||
type RouteUpdateOperation struct {
|
|
||||||
Type RouteUpdateOperationType
|
|
||||||
Values []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoute gets a route object from account and route IDs
|
// GetRoute gets a route object from account and route IDs
|
||||||
func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*route.Route, error) {
|
func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*route.Route, error) {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
@@ -91,30 +38,82 @@ func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*r
|
|||||||
return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
|
return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPrefixPeerExists checks the combination of prefix and peer id, if it exists returns an error, otherwise returns nil
|
// checkRoutePrefixExistsForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups.
|
||||||
func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peerID string, prefix netip.Prefix) error {
|
func (am *DefaultAccountManager) checkRoutePrefixExistsForPeers(account *Account, peerID, routeID string, peerGroupIDs []string, prefix netip.Prefix) error {
|
||||||
|
// routes can have both peer and peer_groups
|
||||||
if peerID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
routesWithPrefix := account.GetRoutesByPrefix(prefix)
|
routesWithPrefix := account.GetRoutesByPrefix(prefix)
|
||||||
|
|
||||||
|
// lets remember all the peers and the peer groups from routesWithPrefix
|
||||||
|
seenPeers := make(map[string]bool)
|
||||||
|
seenPeerGroups := make(map[string]bool)
|
||||||
|
|
||||||
for _, prefixRoute := range routesWithPrefix {
|
for _, prefixRoute := range routesWithPrefix {
|
||||||
if prefixRoute.Peer == peerID {
|
// we skip route(s) with the same network ID as we want to allow updating of the existing route
|
||||||
return status.Errorf(status.AlreadyExists, "failed to add route with prefix %s - peer already has this route", prefix.String())
|
// when create a new route routeID is newly generated so nothing will be skipped
|
||||||
|
if routeID == prefixRoute.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefixRoute.Peer != "" {
|
||||||
|
seenPeers[prefixRoute.ID] = true
|
||||||
|
}
|
||||||
|
for _, groupID := range prefixRoute.PeerGroups {
|
||||||
|
seenPeerGroups[groupID] = true
|
||||||
|
|
||||||
|
group := account.GetGroup(groupID)
|
||||||
|
if group == nil {
|
||||||
|
return status.Errorf(
|
||||||
|
status.InvalidArgument, "failed to add route with prefix %s - peer group %s doesn't exist",
|
||||||
|
prefix.String(), groupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pID := range group.Peers {
|
||||||
|
seenPeers[pID] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if peerID != "" {
|
||||||
|
// check that peerID exists and is not in any route as single peer or part of the group
|
||||||
|
peer := account.GetPeer(peerID)
|
||||||
|
if peer == nil {
|
||||||
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
||||||
|
}
|
||||||
|
if _, ok := seenPeers[peerID]; ok {
|
||||||
|
return status.Errorf(status.AlreadyExists,
|
||||||
|
"failed to add route with prefix %s - peer %s already has this route", prefix.String(), peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that peerGroupIDs are not in any route peerGroups list
|
||||||
|
for _, groupID := range peerGroupIDs {
|
||||||
|
group := account.GetGroup(groupID) // we validated the group existent before entering this function, o need to check again.
|
||||||
|
|
||||||
|
if _, ok := seenPeerGroups[groupID]; ok {
|
||||||
|
return status.Errorf(
|
||||||
|
status.AlreadyExists, "failed to add route with prefix %s - peer group %s already has this route",
|
||||||
|
prefix.String(), group.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix
|
||||||
|
for _, id := range group.Peers {
|
||||||
|
if _, ok := seenPeers[id]; ok {
|
||||||
|
peer := account.GetPeer(id)
|
||||||
|
if peer == nil {
|
||||||
|
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
||||||
|
}
|
||||||
|
return status.Errorf(status.AlreadyExists,
|
||||||
|
"failed to add route with prefix %s - peer %s from the group %s already has this route",
|
||||||
|
prefix.String(), peer.Name, group.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRoute creates and saves a new route
|
// CreateRoute creates and saves a new route
|
||||||
func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) {
|
func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, peerGroupIDs []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
@@ -123,19 +122,29 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if peerID != "" {
|
if peerID != "" && len(peerGroupIDs) != 0 {
|
||||||
peer := account.GetPeer(peerID)
|
return nil, status.Errorf(
|
||||||
if peer == nil {
|
status.InvalidArgument,
|
||||||
return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID)
|
"peer with ID %s and peers group %s should not be provided at the same time",
|
||||||
}
|
peerID, peerGroupIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newRoute route.Route
|
var newRoute route.Route
|
||||||
|
newRoute.ID = xid.New().String()
|
||||||
|
|
||||||
prefixType, newPrefix, err := route.ParseNetwork(network)
|
prefixType, newPrefix, err := route.ParseNetwork(network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse IP %s", network)
|
return nil, status.Errorf(status.InvalidArgument, "failed to parse IP %s", network)
|
||||||
}
|
}
|
||||||
err = am.checkPrefixPeerExists(accountID, peerID, newPrefix)
|
|
||||||
|
if len(peerGroupIDs) > 0 {
|
||||||
|
err = validateGroups(peerGroupIDs, account.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.checkRoutePrefixExistsForPeers(account, peerID, newRoute.ID, peerGroupIDs, newPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -154,7 +163,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
newRoute.Peer = peerID
|
newRoute.Peer = peerID
|
||||||
newRoute.ID = xid.New().String()
|
newRoute.PeerGroups = peerGroupIDs
|
||||||
newRoute.Network = newPrefix
|
newRoute.Network = newPrefix
|
||||||
newRoute.NetworkType = prefixType
|
newRoute.NetworkType = prefixType
|
||||||
newRoute.Description = description
|
newRoute.Description = description
|
||||||
@@ -175,11 +184,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return &newRoute, status.Errorf(status.Internal, "failed to update peers after create route %s", newPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
am.storeEvent(userID, newRoute.ID, accountID, activity.RouteCreated, newRoute.EventMeta())
|
am.storeEvent(userID, newRoute.ID, accountID, activity.RouteCreated, newRoute.EventMeta())
|
||||||
|
|
||||||
@@ -212,13 +217,22 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if routeToSave.Peer != "" {
|
if routeToSave.Peer != "" && len(routeToSave.PeerGroups) != 0 {
|
||||||
peer := account.GetPeer(routeToSave.Peer)
|
return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time")
|
||||||
if peer == nil {
|
}
|
||||||
return status.Errorf(status.InvalidArgument, "peer with ID %s not found", routeToSave.Peer)
|
|
||||||
|
if len(routeToSave.PeerGroups) > 0 {
|
||||||
|
err = validateGroups(routeToSave.PeerGroups, account.Groups)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = am.checkRoutePrefixExistsForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = validateGroups(routeToSave.Groups, account.Groups)
|
err = validateGroups(routeToSave.Groups, account.Groups)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -231,119 +245,13 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
am.storeEvent(userID, routeToSave.ID, accountID, activity.RouteUpdated, routeToSave.EventMeta())
|
am.storeEvent(userID, routeToSave.ID, accountID, activity.RouteUpdated, routeToSave.EventMeta())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRoute updates existing route with set of operations
|
|
||||||
func (am *DefaultAccountManager) UpdateRoute(accountID, routeID string, operations []RouteUpdateOperation) (*route.Route, error) {
|
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
|
||||||
defer unlock()
|
|
||||||
|
|
||||||
account, err := am.Store.GetAccount(accountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
routeToUpdate, ok := account.Routes[routeID]
|
|
||||||
if !ok {
|
|
||||||
return nil, status.Errorf(status.NotFound, "route %s no longer exists", routeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
newRoute := routeToUpdate.Copy()
|
|
||||||
|
|
||||||
for _, operation := range operations {
|
|
||||||
|
|
||||||
if len(operation.Values) != 1 {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "operation %s contains invalid number of values, it should be 1", operation.Type.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
switch operation.Type {
|
|
||||||
case UpdateRouteDescription:
|
|
||||||
newRoute.Description = operation.Values[0]
|
|
||||||
case UpdateRouteNetworkIdentifier:
|
|
||||||
if utf8.RuneCountInString(operation.Values[0]) > route.MaxNetIDChar || operation.Values[0] == "" {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar)
|
|
||||||
}
|
|
||||||
newRoute.NetID = operation.Values[0]
|
|
||||||
case UpdateRouteNetwork:
|
|
||||||
prefixType, prefix, err := route.ParseNetwork(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse IP %s", operation.Values[0])
|
|
||||||
}
|
|
||||||
err = am.checkPrefixPeerExists(accountID, routeToUpdate.Peer, prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newRoute.Network = prefix
|
|
||||||
newRoute.NetworkType = prefixType
|
|
||||||
case UpdateRoutePeer:
|
|
||||||
if operation.Values[0] != "" {
|
|
||||||
peer := account.GetPeer(operation.Values[0])
|
|
||||||
if peer == nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", operation.Values[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = am.checkPrefixPeerExists(accountID, operation.Values[0], routeToUpdate.Network)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newRoute.Peer = operation.Values[0]
|
|
||||||
case UpdateRouteMetric:
|
|
||||||
metric, err := strconv.Atoi(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse metric %s, not int", operation.Values[0])
|
|
||||||
}
|
|
||||||
if metric < route.MinMetric || metric > route.MaxMetric {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse metric %s, value should be %d > N < %d",
|
|
||||||
operation.Values[0],
|
|
||||||
route.MinMetric,
|
|
||||||
route.MaxMetric,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
newRoute.Metric = metric
|
|
||||||
case UpdateRouteMasquerade:
|
|
||||||
masquerade, err := strconv.ParseBool(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse masquerade %s, not boolean", operation.Values[0])
|
|
||||||
}
|
|
||||||
newRoute.Masquerade = masquerade
|
|
||||||
case UpdateRouteEnabled:
|
|
||||||
enabled, err := strconv.ParseBool(operation.Values[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0])
|
|
||||||
}
|
|
||||||
newRoute.Enabled = enabled
|
|
||||||
case UpdateRouteGroups:
|
|
||||||
err = validateGroups(operation.Values, account.Groups)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
newRoute.Groups = operation.Values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
account.Routes[routeID] = newRoute
|
|
||||||
|
|
||||||
account.Network.IncSerial()
|
|
||||||
if err = am.Store.SaveAccount(account); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = am.updateAccountPeers(account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(status.Internal, "failed to update account peers")
|
|
||||||
}
|
|
||||||
return newRoute, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRoute deletes route with routeID
|
// DeleteRoute deletes route with routeID
|
||||||
func (am *DefaultAccountManager) DeleteRoute(accountID, routeID, userID string) error {
|
func (am *DefaultAccountManager) DeleteRoute(accountID, routeID, userID string) error {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
@@ -367,7 +275,9 @@ func (am *DefaultAccountManager) DeleteRoute(accountID, routeID, userID string)
|
|||||||
|
|
||||||
am.storeEvent(userID, routy.ID, accountID, activity.RouteRemoved, routy.EventMeta())
|
am.storeEvent(userID, routy.ID, accountID, activity.RouteRemoved, routy.EventMeta())
|
||||||
|
|
||||||
return am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListRoutes returns a list of routes from account
|
// ListRoutes returns a list of routes from account
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/rs/xid"
|
"github.com/rs/xid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
@@ -14,32 +15,46 @@ import (
|
|||||||
const (
|
const (
|
||||||
peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
|
peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8="
|
||||||
peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
|
peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI="
|
||||||
|
peer3Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NaF="
|
||||||
|
peer4Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5acc="
|
||||||
|
peer5Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5a55="
|
||||||
peer1ID = "peer-1-id"
|
peer1ID = "peer-1-id"
|
||||||
peer2ID = "peer-2-id"
|
peer2ID = "peer-2-id"
|
||||||
|
peer3ID = "peer-3-id"
|
||||||
|
peer4ID = "peer-4-id"
|
||||||
|
peer5ID = "peer-5-id"
|
||||||
routeGroup1 = "routeGroup1"
|
routeGroup1 = "routeGroup1"
|
||||||
routeGroup2 = "routeGroup2"
|
routeGroup2 = "routeGroup2"
|
||||||
|
routeGroup3 = "routeGroup3" // for existing route
|
||||||
|
routeGroup4 = "routeGroup4" // for existing route
|
||||||
|
routeGroupHA1 = "routeGroupHA1"
|
||||||
|
routeGroupHA2 = "routeGroupHA2"
|
||||||
routeInvalidGroup1 = "routeInvalidGroup1"
|
routeInvalidGroup1 = "routeInvalidGroup1"
|
||||||
userID = "testingUser"
|
userID = "testingUser"
|
||||||
|
existingNetwork = "10.10.10.0/24"
|
||||||
|
existingRouteID = "random-id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateRoute(t *testing.T) {
|
func TestCreateRoute(t *testing.T) {
|
||||||
type input struct {
|
type input struct {
|
||||||
network string
|
network string
|
||||||
netID string
|
netID string
|
||||||
peerKey string
|
peerKey string
|
||||||
description string
|
peerGroupIDs []string
|
||||||
masquerade bool
|
description string
|
||||||
metric int
|
masquerade bool
|
||||||
enabled bool
|
metric int
|
||||||
groups []string
|
enabled bool
|
||||||
|
groups []string
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
inputArgs input
|
inputArgs input
|
||||||
shouldCreate bool
|
createInitRoute bool
|
||||||
errFunc require.ErrorAssertionFunc
|
shouldCreate bool
|
||||||
expectedRoute *route.Route
|
errFunc require.ErrorAssertionFunc
|
||||||
|
expectedRoute *route.Route
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Happy Path",
|
name: "Happy Path",
|
||||||
@@ -67,6 +82,48 @@ func TestCreateRoute(t *testing.T) {
|
|||||||
Groups: []string{routeGroup1},
|
Groups: []string{routeGroup1},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Happy Path Peer Groups",
|
||||||
|
inputArgs: input{
|
||||||
|
network: "192.168.0.0/16",
|
||||||
|
netID: "happy",
|
||||||
|
peerGroupIDs: []string{routeGroupHA1, routeGroupHA2},
|
||||||
|
description: "super",
|
||||||
|
masquerade: false,
|
||||||
|
metric: 9999,
|
||||||
|
enabled: true,
|
||||||
|
groups: []string{routeGroup1, routeGroup2},
|
||||||
|
},
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedRoute: &route.Route{
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
NetID: "happy",
|
||||||
|
PeerGroups: []string{routeGroupHA1, routeGroupHA2},
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1, routeGroup2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Both peer and peer_groups Provided Should Fail",
|
||||||
|
inputArgs: input{
|
||||||
|
network: "192.168.0.0/16",
|
||||||
|
netID: "happy",
|
||||||
|
peerKey: peer1ID,
|
||||||
|
peerGroupIDs: []string{routeGroupHA1},
|
||||||
|
description: "super",
|
||||||
|
masquerade: false,
|
||||||
|
metric: 9999,
|
||||||
|
enabled: true,
|
||||||
|
groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Bad Prefix Should Fail",
|
name: "Bad Prefix Should Fail",
|
||||||
inputArgs: input{
|
inputArgs: input{
|
||||||
@@ -97,6 +154,38 @@ func TestCreateRoute(t *testing.T) {
|
|||||||
errFunc: require.Error,
|
errFunc: require.Error,
|
||||||
shouldCreate: false,
|
shouldCreate: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Bad Peer already has this route",
|
||||||
|
inputArgs: input{
|
||||||
|
network: existingNetwork,
|
||||||
|
netID: "bad",
|
||||||
|
peerKey: peer5ID,
|
||||||
|
description: "super",
|
||||||
|
masquerade: false,
|
||||||
|
metric: 9999,
|
||||||
|
enabled: true,
|
||||||
|
groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
createInitRoute: true,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bad Peers Group already has this route",
|
||||||
|
inputArgs: input{
|
||||||
|
network: existingNetwork,
|
||||||
|
netID: "bad",
|
||||||
|
peerGroupIDs: []string{routeGroup1, routeGroup3},
|
||||||
|
description: "super",
|
||||||
|
masquerade: false,
|
||||||
|
metric: 9999,
|
||||||
|
enabled: true,
|
||||||
|
groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
createInitRoute: true,
|
||||||
|
errFunc: require.Error,
|
||||||
|
shouldCreate: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Empty Peer Should Create",
|
name: "Empty Peer Should Create",
|
||||||
inputArgs: input{
|
inputArgs: input{
|
||||||
@@ -238,13 +327,26 @@ func TestCreateRoute(t *testing.T) {
|
|||||||
|
|
||||||
account, err := initTestRouteAccount(t, am)
|
account, err := initTestRouteAccount(t, am)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("failed to init testing account")
|
t.Errorf("failed to init testing account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.createInitRoute {
|
||||||
|
groupAll, errInit := account.GetGroupAll()
|
||||||
|
if errInit != nil {
|
||||||
|
t.Errorf("failed to get group all: %s", errInit)
|
||||||
|
}
|
||||||
|
_, errInit = am.CreateRoute(account.Id, existingNetwork, "", []string{routeGroup3, routeGroup4},
|
||||||
|
"", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID)
|
||||||
|
if errInit != nil {
|
||||||
|
t.Errorf("failed to create init route: %s", errInit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outRoute, err := am.CreateRoute(
|
outRoute, err := am.CreateRoute(
|
||||||
account.Id,
|
account.Id,
|
||||||
testCase.inputArgs.network,
|
testCase.inputArgs.network,
|
||||||
testCase.inputArgs.peerKey,
|
testCase.inputArgs.peerKey,
|
||||||
|
testCase.inputArgs.peerGroupIDs,
|
||||||
testCase.inputArgs.description,
|
testCase.inputArgs.description,
|
||||||
testCase.inputArgs.netID,
|
testCase.inputArgs.netID,
|
||||||
testCase.inputArgs.masquerade,
|
testCase.inputArgs.masquerade,
|
||||||
@@ -272,6 +374,7 @@ func TestCreateRoute(t *testing.T) {
|
|||||||
|
|
||||||
func TestSaveRoute(t *testing.T) {
|
func TestSaveRoute(t *testing.T) {
|
||||||
validPeer := peer2ID
|
validPeer := peer2ID
|
||||||
|
validUsedPeer := peer5ID
|
||||||
invalidPeer := "nonExisting"
|
invalidPeer := "nonExisting"
|
||||||
validPrefix := netip.MustParsePrefix("192.168.0.0/24")
|
validPrefix := netip.MustParsePrefix("192.168.0.0/24")
|
||||||
invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34")
|
invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34")
|
||||||
@@ -279,18 +382,22 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
invalidMetric := 99999
|
invalidMetric := 99999
|
||||||
validNetID := "12345678901234567890qw"
|
validNetID := "12345678901234567890qw"
|
||||||
invalidNetID := "12345678901234567890qwertyuiopqwertyuiop1"
|
invalidNetID := "12345678901234567890qwertyuiopqwertyuiop1"
|
||||||
|
validGroupHA1 := routeGroupHA1
|
||||||
|
validGroupHA2 := routeGroupHA2
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
existingRoute *route.Route
|
existingRoute *route.Route
|
||||||
newPeer *string
|
createInitRoute bool
|
||||||
newMetric *int
|
newPeer *string
|
||||||
newPrefix *netip.Prefix
|
newPeerGroups []string
|
||||||
newGroups []string
|
newMetric *int
|
||||||
skipCopying bool
|
newPrefix *netip.Prefix
|
||||||
shouldCreate bool
|
newGroups []string
|
||||||
errFunc require.ErrorAssertionFunc
|
skipCopying bool
|
||||||
expectedRoute *route.Route
|
shouldCreate bool
|
||||||
|
errFunc require.ErrorAssertionFunc
|
||||||
|
expectedRoute *route.Route
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Happy Path",
|
name: "Happy Path",
|
||||||
@@ -325,6 +432,55 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
Groups: []string{routeGroup2},
|
Groups: []string{routeGroup2},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Happy Path Peer Groups",
|
||||||
|
existingRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
newPeerGroups: []string{validGroupHA1, validGroupHA2},
|
||||||
|
newMetric: &validMetric,
|
||||||
|
newPrefix: &validPrefix,
|
||||||
|
newGroups: []string{routeGroup2},
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: validPrefix,
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
PeerGroups: []string{validGroupHA1, validGroupHA2},
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: validMetric,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup2},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Both peer and peers_roup Provided Should Fail",
|
||||||
|
existingRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
newPeer: &validPeer,
|
||||||
|
newPeerGroups: []string{validGroupHA1},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Bad Prefix Should Fail",
|
name: "Bad Prefix Should Fail",
|
||||||
existingRoute: &route.Route{
|
existingRoute: &route.Route{
|
||||||
@@ -461,6 +617,73 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
newGroups: []string{routeInvalidGroup1},
|
newGroups: []string{routeInvalidGroup1},
|
||||||
errFunc: require.Error,
|
errFunc: require.Error,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Allow to modify existing route with new peer",
|
||||||
|
existingRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix(existingNetwork),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Peer: peer1ID,
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
newPeer: &validPeer,
|
||||||
|
errFunc: require.NoError,
|
||||||
|
shouldCreate: true,
|
||||||
|
expectedRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix(existingNetwork),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Peer: validPeer,
|
||||||
|
PeerGroups: []string{},
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Do not allow to modify existing route with a peer from another route",
|
||||||
|
existingRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix(existingNetwork),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Peer: peer1ID,
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
createInitRoute: true,
|
||||||
|
newPeer: &validUsedPeer,
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Do not allow to modify existing route with a peers group from another route",
|
||||||
|
existingRoute: &route.Route{
|
||||||
|
ID: "testingRoute",
|
||||||
|
Network: netip.MustParsePrefix(existingNetwork),
|
||||||
|
NetID: validNetID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
PeerGroups: []string{routeGroup3},
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
},
|
||||||
|
createInitRoute: true,
|
||||||
|
newPeerGroups: []string{routeGroup4},
|
||||||
|
errFunc: require.Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
@@ -474,6 +697,21 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
t.Error("failed to init testing account")
|
t.Error("failed to init testing account")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if testCase.createInitRoute {
|
||||||
|
account.Routes["initRoute"] = &route.Route{
|
||||||
|
ID: "initRoute",
|
||||||
|
Network: netip.MustParsePrefix(existingNetwork),
|
||||||
|
NetID: existingRouteID,
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
PeerGroups: []string{routeGroup4},
|
||||||
|
Description: "super",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
account.Routes[testCase.existingRoute.ID] = testCase.existingRoute
|
account.Routes[testCase.existingRoute.ID] = testCase.existingRoute
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
err = am.Store.SaveAccount(account)
|
||||||
@@ -488,6 +726,9 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
if testCase.newPeer != nil {
|
if testCase.newPeer != nil {
|
||||||
routeToSave.Peer = *testCase.newPeer
|
routeToSave.Peer = *testCase.newPeer
|
||||||
}
|
}
|
||||||
|
if len(testCase.newPeerGroups) != 0 {
|
||||||
|
routeToSave.PeerGroups = testCase.newPeerGroups
|
||||||
|
}
|
||||||
if testCase.newMetric != nil {
|
if testCase.newMetric != nil {
|
||||||
routeToSave.Metric = *testCase.newMetric
|
routeToSave.Metric = *testCase.newMetric
|
||||||
}
|
}
|
||||||
@@ -524,265 +765,6 @@ func TestSaveRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateRoute(t *testing.T) {
|
|
||||||
routeID := "testingRouteID"
|
|
||||||
|
|
||||||
existingRoute := &route.Route{
|
|
||||||
ID: routeID,
|
|
||||||
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
NetID: "superRoute",
|
|
||||||
NetworkType: route.IPv4Network,
|
|
||||||
Peer: peer1ID,
|
|
||||||
Description: "super",
|
|
||||||
Masquerade: false,
|
|
||||||
Metric: 9999,
|
|
||||||
Enabled: true,
|
|
||||||
Groups: []string{routeGroup1},
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
existingRoute *route.Route
|
|
||||||
operations []RouteUpdateOperation
|
|
||||||
shouldCreate bool
|
|
||||||
errFunc require.ErrorAssertionFunc
|
|
||||||
expectedRoute *route.Route
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Happy Path Single OPS",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
Values: []string{peer2ID},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.NoError,
|
|
||||||
shouldCreate: true,
|
|
||||||
expectedRoute: &route.Route{
|
|
||||||
ID: routeID,
|
|
||||||
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
NetID: "superRoute",
|
|
||||||
NetworkType: route.IPv4Network,
|
|
||||||
Peer: peer2ID,
|
|
||||||
Description: "super",
|
|
||||||
Masquerade: false,
|
|
||||||
Metric: 9999,
|
|
||||||
Enabled: true,
|
|
||||||
Groups: []string{routeGroup1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Happy Path Multiple OPS",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteDescription,
|
|
||||||
Values: []string{"great"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteNetwork,
|
|
||||||
Values: []string{"192.168.0.0/24"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
Values: []string{peer2ID},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteMetric,
|
|
||||||
Values: []string{"3030"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteMasquerade,
|
|
||||||
Values: []string{"true"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteEnabled,
|
|
||||||
Values: []string{"false"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteNetworkIdentifier,
|
|
||||||
Values: []string{"megaRoute"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: UpdateRouteGroups,
|
|
||||||
Values: []string{routeGroup2},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.NoError,
|
|
||||||
shouldCreate: true,
|
|
||||||
expectedRoute: &route.Route{
|
|
||||||
ID: routeID,
|
|
||||||
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
|
||||||
NetID: "megaRoute",
|
|
||||||
NetworkType: route.IPv4Network,
|
|
||||||
Peer: peer2ID,
|
|
||||||
Description: "great",
|
|
||||||
Masquerade: true,
|
|
||||||
Metric: 3030,
|
|
||||||
Enabled: false,
|
|
||||||
Groups: []string{routeGroup2},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Values Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Multiple Values Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
Values: []string{peer2ID, peer1ID},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bad Prefix Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteNetwork,
|
|
||||||
Values: []string{"192.168.0.0/34"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bad Peer Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
Values: []string{"non existing Peer"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Peer",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRoutePeer,
|
|
||||||
Values: []string{""},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.NoError,
|
|
||||||
shouldCreate: true,
|
|
||||||
expectedRoute: &route.Route{
|
|
||||||
ID: routeID,
|
|
||||||
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
NetID: "superRoute",
|
|
||||||
NetworkType: route.IPv4Network,
|
|
||||||
Peer: "",
|
|
||||||
Description: "super",
|
|
||||||
Masquerade: false,
|
|
||||||
Metric: 9999,
|
|
||||||
Enabled: true,
|
|
||||||
Groups: []string{routeGroup1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Large Network ID Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteNetworkIdentifier,
|
|
||||||
Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty Network ID Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteNetworkIdentifier,
|
|
||||||
Values: []string{""},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Metric Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteMetric,
|
|
||||||
Values: []string{"999999"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Boolean Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteMasquerade,
|
|
||||||
Values: []string{"yes"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid Group Should Fail",
|
|
||||||
existingRoute: existingRoute,
|
|
||||||
operations: []RouteUpdateOperation{
|
|
||||||
{
|
|
||||||
Type: UpdateRouteGroups,
|
|
||||||
Values: []string{routeInvalidGroup1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errFunc: require.Error,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
am, err := createRouterManager(t)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("failed to create account manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := initTestRouteAccount(t, am)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("failed to init testing account")
|
|
||||||
}
|
|
||||||
|
|
||||||
account.Routes[testCase.existingRoute.ID] = testCase.existingRoute
|
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("account should be saved")
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedRoute, err := am.UpdateRoute(account.Id, testCase.existingRoute.ID, testCase.operations)
|
|
||||||
|
|
||||||
testCase.errFunc(t, err)
|
|
||||||
|
|
||||||
if !testCase.shouldCreate {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
testCase.expectedRoute.ID = updatedRoute.ID
|
|
||||||
|
|
||||||
if !testCase.expectedRoute.IsEqual(updatedRoute) {
|
|
||||||
t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedRoute)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteRoute(t *testing.T) {
|
func TestDeleteRoute(t *testing.T) {
|
||||||
testingRoute := &route.Route{
|
testingRoute := &route.Route{
|
||||||
ID: "testingRoute",
|
ID: "testingRoute",
|
||||||
@@ -828,6 +810,96 @@ func TestDeleteRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) {
|
||||||
|
baseRoute := &route.Route{
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
NetID: "superNet",
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
PeerGroups: []string{routeGroupHA1, routeGroupHA2},
|
||||||
|
Description: "ha route",
|
||||||
|
Masquerade: false,
|
||||||
|
Metric: 9999,
|
||||||
|
Enabled: true,
|
||||||
|
Groups: []string{routeGroup1, routeGroup2},
|
||||||
|
}
|
||||||
|
|
||||||
|
am, err := createRouterManager(t)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to create account manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := initTestRouteAccount(t, am)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("failed to init testing account")
|
||||||
|
}
|
||||||
|
|
||||||
|
newAccountRoutes, err := am.GetNetworkMap(peer1ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes")
|
||||||
|
|
||||||
|
newRoute, err := am.CreateRoute(
|
||||||
|
account.Id, baseRoute.Network.String(), baseRoute.Peer, baseRoute.PeerGroups, baseRoute.Description,
|
||||||
|
baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, baseRoute.Enabled, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, newRoute.Enabled, true)
|
||||||
|
|
||||||
|
peer1Routes, err := am.GetNetworkMap(peer1ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer1Routes.Routes, 1, "HA route should have 1 server route")
|
||||||
|
|
||||||
|
peer2Routes, err := am.GetNetworkMap(peer2ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer2Routes.Routes, 1, "HA route should have 1 server route")
|
||||||
|
|
||||||
|
peer4Routes, err := am.GetNetworkMap(peer4ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer4Routes.Routes, 1, "HA route should have 1 server route")
|
||||||
|
|
||||||
|
groups, err := am.ListGroups(account.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var groupHA1, groupHA2 *Group
|
||||||
|
for _, group := range groups {
|
||||||
|
switch group.Name {
|
||||||
|
case routeGroupHA1:
|
||||||
|
groupHA1 = group
|
||||||
|
case routeGroupHA2:
|
||||||
|
groupHA2 = group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.GroupDeletePeer(account.Id, groupHA1.ID, peer2ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer2RoutesAfterDelete, err := am.GetNetworkMap(peer2ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer2RoutesAfterDelete.Routes, 2, "after peer deletion group should have 2 client routes")
|
||||||
|
|
||||||
|
err = am.GroupDeletePeer(account.Id, groupHA2.ID, peer4ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer2RoutesAfterDelete, err = am.GetNetworkMap(peer2ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer2RoutesAfterDelete.Routes, 1, "after peer deletion group should have only 1 route")
|
||||||
|
|
||||||
|
err = am.GroupAddPeer(account.Id, groupHA2.ID, peer4ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer1RoutesAfterAdd, err := am.GetNetworkMap(peer1ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer1RoutesAfterAdd.Routes, 1, "HA route should have more than 1 route")
|
||||||
|
|
||||||
|
peer2RoutesAfterAdd, err := am.GetNetworkMap(peer2ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer2RoutesAfterAdd.Routes, 2, "HA route should have 2 client routes")
|
||||||
|
|
||||||
|
err = am.DeleteRoute(account.Id, newRoute.ID, userID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
peer1DeletedRoute, err := am.GetNetworkMap(peer1ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, peer1DeletedRoute.Routes, 0, "we should receive one route for peer1")
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetNetworkMap_RouteSync(t *testing.T) {
|
func TestGetNetworkMap_RouteSync(t *testing.T) {
|
||||||
// no routes for peer in different groups
|
// no routes for peer in different groups
|
||||||
// no routes when route is deleted
|
// no routes when route is deleted
|
||||||
@@ -858,7 +930,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes")
|
require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes")
|
||||||
|
|
||||||
createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), peer1ID,
|
createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), peer1ID, []string{},
|
||||||
baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false,
|
baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false,
|
||||||
userID)
|
userID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -940,7 +1012,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore)
|
return BuildManager(store, NewPeersUpdateManager(), nil, "", "", eventStore, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRouterStore(t *testing.T) (Store, error) {
|
func createRouterStore(t *testing.T) (Store, error) {
|
||||||
@@ -954,6 +1026,8 @@ func createRouterStore(t *testing.T) (Store, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) {
|
func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
accountID := "testingAcc"
|
accountID := "testingAcc"
|
||||||
domain := "example.com"
|
domain := "example.com"
|
||||||
|
|
||||||
@@ -1013,6 +1087,81 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
|
|||||||
}
|
}
|
||||||
account.Peers[peer2.ID] = peer2
|
account.Peers[peer2.ID] = peer2
|
||||||
|
|
||||||
|
ips = account.getTakenIPs()
|
||||||
|
peer3IP, err := AllocatePeerIP(account.Network.Net, ips)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer3 := &Peer{
|
||||||
|
IP: peer3IP,
|
||||||
|
ID: peer3ID,
|
||||||
|
Key: peer3Key,
|
||||||
|
Name: "test-host3@netbird.io",
|
||||||
|
UserID: userID,
|
||||||
|
Meta: PeerSystemMeta{
|
||||||
|
Hostname: "test-host3@netbird.io",
|
||||||
|
GoOS: "darwin",
|
||||||
|
Kernel: "Darwin",
|
||||||
|
Core: "13.4.1",
|
||||||
|
Platform: "arm64",
|
||||||
|
OS: "darwin",
|
||||||
|
WtVersion: "development",
|
||||||
|
UIVersion: "development",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
account.Peers[peer3.ID] = peer3
|
||||||
|
|
||||||
|
ips = account.getTakenIPs()
|
||||||
|
peer4IP, err := AllocatePeerIP(account.Network.Net, ips)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer4 := &Peer{
|
||||||
|
IP: peer4IP,
|
||||||
|
ID: peer4ID,
|
||||||
|
Key: peer4Key,
|
||||||
|
Name: "test-host4@netbird.io",
|
||||||
|
UserID: userID,
|
||||||
|
Meta: PeerSystemMeta{
|
||||||
|
Hostname: "test-host4@netbird.io",
|
||||||
|
GoOS: "linux",
|
||||||
|
Kernel: "Linux",
|
||||||
|
Core: "21.04",
|
||||||
|
Platform: "x86_64",
|
||||||
|
OS: "Ubuntu",
|
||||||
|
WtVersion: "development",
|
||||||
|
UIVersion: "development",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
account.Peers[peer4.ID] = peer4
|
||||||
|
|
||||||
|
ips = account.getTakenIPs()
|
||||||
|
peer5IP, err := AllocatePeerIP(account.Network.Net, ips)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
peer5 := &Peer{
|
||||||
|
IP: peer5IP,
|
||||||
|
ID: peer5ID,
|
||||||
|
Key: peer5Key,
|
||||||
|
Name: "test-host4@netbird.io",
|
||||||
|
UserID: userID,
|
||||||
|
Meta: PeerSystemMeta{
|
||||||
|
Hostname: "test-host4@netbird.io",
|
||||||
|
GoOS: "linux",
|
||||||
|
Kernel: "Linux",
|
||||||
|
Core: "21.04",
|
||||||
|
Platform: "x86_64",
|
||||||
|
OS: "Ubuntu",
|
||||||
|
WtVersion: "development",
|
||||||
|
UIVersion: "development",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
account.Peers[peer5.ID] = peer5
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
err = am.Store.SaveAccount(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1029,26 +1178,53 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
err = am.GroupAddPeer(accountID, groupAll.ID, peer3ID)
|
||||||
newGroup := &Group{
|
if err != nil {
|
||||||
ID: routeGroup1,
|
return nil, err
|
||||||
Name: routeGroup1,
|
|
||||||
Peers: []string{peer1.ID},
|
|
||||||
}
|
}
|
||||||
err = am.SaveGroup(accountID, userID, newGroup)
|
err = am.GroupAddPeer(accountID, groupAll.ID, peer4ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newGroup = &Group{
|
newGroup := []*Group{
|
||||||
ID: routeGroup2,
|
{
|
||||||
Name: routeGroup2,
|
ID: routeGroup1,
|
||||||
Peers: []string{peer2.ID},
|
Name: routeGroup1,
|
||||||
|
Peers: []string{peer1.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: routeGroup2,
|
||||||
|
Name: routeGroup2,
|
||||||
|
Peers: []string{peer2.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: routeGroup3,
|
||||||
|
Name: routeGroup3,
|
||||||
|
Peers: []string{peer5.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: routeGroup4,
|
||||||
|
Name: routeGroup4,
|
||||||
|
Peers: []string{peer5.ID},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: routeGroupHA1,
|
||||||
|
Name: routeGroupHA1,
|
||||||
|
Peers: []string{peer1.ID, peer2.ID, peer3.ID}, // we have one non Linux peer, see peer3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: routeGroupHA2,
|
||||||
|
Name: routeGroupHA2,
|
||||||
|
Peers: []string{peer1.ID, peer4.ID},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = am.SaveGroup(accountID, userID, newGroup)
|
for _, group := range newGroup {
|
||||||
if err != nil {
|
err = am.SaveGroup(accountID, userID, group)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return am.Store.GetAccount(account.Id)
|
return am.Store.GetAccount(account.Id)
|
||||||
|
|||||||
@@ -317,7 +317,9 @@ func (am *DefaultAccountManager) SaveSetupKey(accountID string, keyToSave *Setup
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return newKey, am.updateAccountPeers(account)
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
|
return newKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSetupKeys returns a list of all setup keys of the account
|
// ListSetupKeys returns a list of all setup keys of the account
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type GRPCMetrics struct {
|
|||||||
activeStreamsGauge asyncint64.Gauge
|
activeStreamsGauge asyncint64.Gauge
|
||||||
syncRequestDuration syncint64.Histogram
|
syncRequestDuration syncint64.Histogram
|
||||||
loginRequestDuration syncint64.Histogram
|
loginRequestDuration syncint64.Histogram
|
||||||
|
channelQueueLength syncint64.Histogram
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,18 @@ func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, erro
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We use histogram here as we have multiple channel at the same time and we want to see a slice at any given time
|
||||||
|
// Then we should be able to extract min, manx, mean and the percentiles.
|
||||||
|
// TODO(yury): This needs custom bucketing as we are interested in the values from 0 to server.channelBufferSize (100)
|
||||||
|
channelQueue, err := meter.SyncInt64().Histogram(
|
||||||
|
"management.grpc.updatechannel.queue",
|
||||||
|
instrument.WithDescription("Number of update messages in the channel queue"),
|
||||||
|
instrument.WithUnit("length"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &GRPCMetrics{
|
return &GRPCMetrics{
|
||||||
meter: meter,
|
meter: meter,
|
||||||
syncRequestsCounter: syncRequestsCounter,
|
syncRequestsCounter: syncRequestsCounter,
|
||||||
@@ -60,6 +73,7 @@ func NewGRPCMetrics(ctx context.Context, meter metric.Meter) (*GRPCMetrics, erro
|
|||||||
activeStreamsGauge: activeStreamsGauge,
|
activeStreamsGauge: activeStreamsGauge,
|
||||||
syncRequestDuration: syncRequestDuration,
|
syncRequestDuration: syncRequestDuration,
|
||||||
loginRequestDuration: loginRequestDuration,
|
loginRequestDuration: loginRequestDuration,
|
||||||
|
channelQueueLength: channelQueue,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
@@ -100,3 +114,8 @@ func (grpcMetrics *GRPCMetrics) RegisterConnectedStreams(producer func() int64)
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateChannelQueueLength update the histogram that keep distribution of the update messages channel queue
|
||||||
|
func (metrics *GRPCMetrics) UpdateChannelQueueLength(len int) {
|
||||||
|
metrics.channelQueueLength.Record(metrics.ctx, int64(len))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package telemetry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"go.opentelemetry.io/otel/metric/instrument"
|
"go.opentelemetry.io/otel/metric/instrument"
|
||||||
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
"go.opentelemetry.io/otel/metric/instrument/syncint64"
|
||||||
@@ -13,6 +14,7 @@ type IDPMetrics struct {
|
|||||||
getUserByEmailCounter syncint64.Counter
|
getUserByEmailCounter syncint64.Counter
|
||||||
getAllAccountsCounter syncint64.Counter
|
getAllAccountsCounter syncint64.Counter
|
||||||
createUserCounter syncint64.Counter
|
createUserCounter syncint64.Counter
|
||||||
|
deleteUserCounter syncint64.Counter
|
||||||
getAccountCounter syncint64.Counter
|
getAccountCounter syncint64.Counter
|
||||||
getUserByIDCounter syncint64.Counter
|
getUserByIDCounter syncint64.Counter
|
||||||
authenticateRequestCounter syncint64.Counter
|
authenticateRequestCounter syncint64.Counter
|
||||||
@@ -39,6 +41,10 @@ func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
deleteUserCounter, err := meter.SyncInt64().Counter("management.idp.delete.user.counter", instrument.WithUnit("1"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
getAccountCounter, err := meter.SyncInt64().Counter("management.idp.get.account.counter", instrument.WithUnit("1"))
|
getAccountCounter, err := meter.SyncInt64().Counter("management.idp.get.account.counter", instrument.WithUnit("1"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -65,6 +71,7 @@ func NewIDPMetrics(ctx context.Context, meter metric.Meter) (*IDPMetrics, error)
|
|||||||
getUserByEmailCounter: getUserByEmailCounter,
|
getUserByEmailCounter: getUserByEmailCounter,
|
||||||
getAllAccountsCounter: getAllAccountsCounter,
|
getAllAccountsCounter: getAllAccountsCounter,
|
||||||
createUserCounter: createUserCounter,
|
createUserCounter: createUserCounter,
|
||||||
|
deleteUserCounter: deleteUserCounter,
|
||||||
getAccountCounter: getAccountCounter,
|
getAccountCounter: getAccountCounter,
|
||||||
getUserByIDCounter: getUserByIDCounter,
|
getUserByIDCounter: getUserByIDCounter,
|
||||||
authenticateRequestCounter: authenticateRequestCounter,
|
authenticateRequestCounter: authenticateRequestCounter,
|
||||||
@@ -88,6 +95,11 @@ func (idpMetrics *IDPMetrics) CountCreateUser() {
|
|||||||
idpMetrics.createUserCounter.Add(idpMetrics.ctx, 1)
|
idpMetrics.createUserCounter.Add(idpMetrics.ctx, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountDeleteUser ...
|
||||||
|
func (idpMetrics *IDPMetrics) CountDeleteUser() {
|
||||||
|
idpMetrics.deleteUserCounter.Add(idpMetrics.ctx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
// CountGetAllAccounts ...
|
// CountGetAllAccounts ...
|
||||||
func (idpMetrics *IDPMetrics) CountGetAllAccounts() {
|
func (idpMetrics *IDPMetrics) CountGetAllAccounts() {
|
||||||
idpMetrics.getAllAccountsCounter.Add(idpMetrics.ctx, 1)
|
idpMetrics.getAllAccountsCounter.Add(idpMetrics.ctx, 1)
|
||||||
|
|||||||
@@ -11,37 +11,54 @@ import (
|
|||||||
|
|
||||||
// StoreMetrics represents all metrics related to the FileStore
|
// StoreMetrics represents all metrics related to the FileStore
|
||||||
type StoreMetrics struct {
|
type StoreMetrics struct {
|
||||||
globalLockAcquisitionDuration syncint64.Histogram
|
globalLockAcquisitionDurationMicro syncint64.Histogram
|
||||||
persistenceDuration syncint64.Histogram
|
globalLockAcquisitionDurationMs syncint64.Histogram
|
||||||
ctx context.Context
|
persistenceDurationMicro syncint64.Histogram
|
||||||
|
persistenceDurationMs syncint64.Histogram
|
||||||
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStoreMetrics creates an instance of StoreMetrics
|
// NewStoreMetrics creates an instance of StoreMetrics
|
||||||
func NewStoreMetrics(ctx context.Context, meter metric.Meter) (*StoreMetrics, error) {
|
func NewStoreMetrics(ctx context.Context, meter metric.Meter) (*StoreMetrics, error) {
|
||||||
globalLockAcquisitionDuration, err := meter.SyncInt64().Histogram("management.store.global.lock.acquisition.duration.micro",
|
globalLockAcquisitionDurationMicro, err := meter.SyncInt64().Histogram("management.store.global.lock.acquisition.duration.micro",
|
||||||
instrument.WithUnit("microseconds"))
|
instrument.WithUnit("microseconds"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
persistenceDuration, err := meter.SyncInt64().Histogram("management.store.persistence.duration.micro",
|
|
||||||
|
globalLockAcquisitionDurationMs, err := meter.SyncInt64().Histogram("management.store.global.lock.acquisition.duration.ms")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
persistenceDurationMicro, err := meter.SyncInt64().Histogram("management.store.persistence.duration.micro",
|
||||||
instrument.WithUnit("microseconds"))
|
instrument.WithUnit("microseconds"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistenceDurationMs, err := meter.SyncInt64().Histogram("management.store.persistence.duration.ms")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &StoreMetrics{
|
return &StoreMetrics{
|
||||||
globalLockAcquisitionDuration: globalLockAcquisitionDuration,
|
globalLockAcquisitionDurationMicro: globalLockAcquisitionDurationMicro,
|
||||||
persistenceDuration: persistenceDuration,
|
globalLockAcquisitionDurationMs: globalLockAcquisitionDurationMs,
|
||||||
ctx: ctx,
|
persistenceDurationMicro: persistenceDurationMicro,
|
||||||
|
persistenceDurationMs: persistenceDurationMs,
|
||||||
|
ctx: ctx,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountGlobalLockAcquisitionDuration counts the duration of the global lock acquisition
|
// CountGlobalLockAcquisitionDuration counts the duration of the global lock acquisition
|
||||||
func (metrics *StoreMetrics) CountGlobalLockAcquisitionDuration(duration time.Duration) {
|
func (metrics *StoreMetrics) CountGlobalLockAcquisitionDuration(duration time.Duration) {
|
||||||
metrics.globalLockAcquisitionDuration.Record(metrics.ctx, duration.Microseconds())
|
metrics.globalLockAcquisitionDurationMicro.Record(metrics.ctx, duration.Microseconds())
|
||||||
|
metrics.globalLockAcquisitionDurationMs.Record(metrics.ctx, duration.Milliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountPersistenceDuration counts the duration of a store persistence operation
|
// CountPersistenceDuration counts the duration of a store persistence operation
|
||||||
func (metrics *StoreMetrics) CountPersistenceDuration(duration time.Duration) {
|
func (metrics *StoreMetrics) CountPersistenceDuration(duration time.Duration) {
|
||||||
metrics.persistenceDuration.Record(metrics.ctx, duration.Microseconds())
|
metrics.persistenceDurationMicro.Record(metrics.ctx, duration.Microseconds())
|
||||||
|
metrics.persistenceDurationMs.Record(metrics.ctx, duration.Milliseconds())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,11 +118,7 @@ func (m *TimeBasedAuthSecretsManager) SetupRefresh(peerID string) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
log.Debugf("sending new TURN credentials to peer %s", peerID)
|
log.Debugf("sending new TURN credentials to peer %s", peerID)
|
||||||
err := m.updateManager.SendUpdate(peerID, &UpdateMessage{Update: update})
|
m.updateManager.SendUpdate(peerID, &UpdateMessage{Update: update})
|
||||||
if err != nil {
|
|
||||||
log.Errorf("error while sending TURN update to peer %s %v", peerID, err)
|
|
||||||
// todo maybe continue trying?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func NewPeersUpdateManager() *PeersUpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendUpdate sends update message to the peer's channel
|
// SendUpdate sends update message to the peer's channel
|
||||||
func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) error {
|
func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) {
|
||||||
p.channelsMux.Lock()
|
p.channelsMux.Lock()
|
||||||
defer p.channelsMux.Unlock()
|
defer p.channelsMux.Unlock()
|
||||||
if channel, ok := p.peerChannels[peerID]; ok {
|
if channel, ok := p.peerChannels[peerID]; ok {
|
||||||
@@ -39,10 +39,9 @@ func (p *PeersUpdateManager) SendUpdate(peerID string, update *UpdateMessage) er
|
|||||||
default:
|
default:
|
||||||
log.Warnf("channel for peer %s is %d full", peerID, len(channel))
|
log.Warnf("channel for peer %s is %d full", peerID, len(channel))
|
||||||
}
|
}
|
||||||
return nil
|
} else {
|
||||||
|
log.Debugf("peer %s has no channel", peerID)
|
||||||
}
|
}
|
||||||
log.Debugf("peer %s has no channel", peerID)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateChannel creates a go channel for a given peer used to deliver updates relevant to the peer.
|
// CreateChannel creates a go channel for a given peer used to deliver updates relevant to the peer.
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ func TestSendUpdate(t *testing.T) {
|
|||||||
if _, ok := peersUpdater.peerChannels[peer]; !ok {
|
if _, ok := peersUpdater.peerChannels[peer]; !ok {
|
||||||
t.Error("Error creating the channel")
|
t.Error("Error creating the channel")
|
||||||
}
|
}
|
||||||
err := peersUpdater.SendUpdate(peer, update1)
|
peersUpdater.SendUpdate(peer, update1)
|
||||||
if err != nil {
|
|
||||||
t.Error("Error sending update: ", err)
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case <-peersUpdater.peerChannels[peer]:
|
case <-peersUpdater.peerChannels[peer]:
|
||||||
default:
|
default:
|
||||||
@@ -42,10 +39,7 @@ func TestSendUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for range [channelBufferSize]int{} {
|
for range [channelBufferSize]int{} {
|
||||||
err = peersUpdater.SendUpdate(peer, update1)
|
peersUpdater.SendUpdate(peer, update1)
|
||||||
if err != nil {
|
|
||||||
t.Errorf("got an early error sending update: %v ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update2 := &UpdateMessage{Update: &proto.SyncResponse{
|
update2 := &UpdateMessage{Update: &proto.SyncResponse{
|
||||||
@@ -54,10 +48,7 @@ func TestSendUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
err = peersUpdater.SendUpdate(peer, update2)
|
peersUpdater.SendUpdate(peer, update2)
|
||||||
if err != nil {
|
|
||||||
t.Error("update shouldn't return an error when channel buffer is full")
|
|
||||||
}
|
|
||||||
timeout := time.After(5 * time.Second)
|
timeout := time.After(5 * time.Second)
|
||||||
for range [channelBufferSize]int{} {
|
for range [channelBufferSize]int{} {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -307,8 +307,17 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) deleteServiceUser(account *Account, initiatorUserID string, targetUser *User) {
|
||||||
|
meta := map[string]any{"name": targetUser.ServiceUserName}
|
||||||
|
am.storeEvent(initiatorUserID, targetUser.Id, account.Id, activity.ServiceUserDeleted, meta)
|
||||||
|
delete(account.Users, targetUser.Id)
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteUser deletes a user from the given account.
|
// DeleteUser deletes a user from the given account.
|
||||||
func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, targetUserID string) error {
|
func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, targetUserID string) error {
|
||||||
|
if initiatorUserID == targetUserID {
|
||||||
|
return status.Errorf(status.InvalidArgument, "self deletion is not allowed")
|
||||||
|
}
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
defer unlock()
|
defer unlock()
|
||||||
|
|
||||||
@@ -317,36 +326,76 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
targetUser := account.Users[targetUserID]
|
|
||||||
if targetUser == nil {
|
|
||||||
return status.Errorf(status.NotFound, "user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
executingUser := account.Users[initiatorUserID]
|
executingUser := account.Users[initiatorUserID]
|
||||||
if executingUser == nil {
|
if executingUser == nil {
|
||||||
return status.Errorf(status.NotFound, "user not found")
|
return status.Errorf(status.NotFound, "user not found")
|
||||||
}
|
}
|
||||||
if executingUser.Role != UserRoleAdmin {
|
if executingUser.Role != UserRoleAdmin {
|
||||||
return status.Errorf(status.PermissionDenied, "only admins can delete service users")
|
return status.Errorf(status.PermissionDenied, "only admins can delete users")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !targetUser.IsServiceUser {
|
targetUser := account.Users[targetUserID]
|
||||||
return status.Errorf(status.PermissionDenied, "regular users can not be deleted")
|
if targetUser == nil {
|
||||||
|
return status.Errorf(status.NotFound, "target user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := map[string]any{"name": targetUser.ServiceUserName}
|
// handle service user first and exit, no need to fetch extra data from IDP, etc
|
||||||
am.storeEvent(initiatorUserID, targetUserID, accountID, activity.ServiceUserDeleted, meta)
|
if targetUser.IsServiceUser {
|
||||||
|
am.deleteServiceUser(account, initiatorUserID, targetUser)
|
||||||
|
return am.Store.SaveAccount(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.deleteRegularUser(account, initiatorUserID, targetUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) deleteRegularUser(account *Account, initiatorUserID, targetUserID string) error {
|
||||||
|
tuEmail, tuName, err := am.getEmailAndNameOfTargetUser(account.Id, initiatorUserID, targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to resolve email address: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isNil(am.idpManager) {
|
||||||
|
err = am.deleteUserFromIDP(targetUserID, account.Id)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to delete user from IDP: %s", targetUserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.deleteUserPeers(initiatorUserID, targetUserID, account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
delete(account.Users, targetUserID)
|
delete(account.Users, targetUserID)
|
||||||
|
|
||||||
err = am.Store.SaveAccount(account)
|
err = am.Store.SaveAccount(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meta := map[string]any{"name": tuName, "email": tuEmail}
|
||||||
|
am.storeEvent(initiatorUserID, targetUserID, account.Id, activity.UserDeleted, meta)
|
||||||
|
|
||||||
|
am.updateAccountPeers(account)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) deleteUserPeers(initiatorUserID string, targetUserID string, account *Account) error {
|
||||||
|
peers, err := account.FindUserPeers(targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return status.Errorf(status.Internal, "failed to find user peers")
|
||||||
|
}
|
||||||
|
|
||||||
|
peerIDs := make([]string, 0, len(peers))
|
||||||
|
for _, peer := range peers {
|
||||||
|
peerIDs = append(peerIDs, peer.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.deletePeers(account, peerIDs, initiatorUserID)
|
||||||
|
}
|
||||||
|
|
||||||
// InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period.
|
// InviteUser resend invitations to users who haven't activated their accounts prior to the expiration period.
|
||||||
func (am *DefaultAccountManager) InviteUser(accountID string, initiatorUserID string, targetUserID string) error {
|
func (am *DefaultAccountManager) InviteUser(accountID string, initiatorUserID string, targetUserID string) error {
|
||||||
unlock := am.Store.AcquireAccountLock(accountID)
|
unlock := am.Store.AcquireAccountLock(accountID)
|
||||||
@@ -609,23 +658,10 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var peerIDs []string
|
|
||||||
for _, peer := range blockedPeers {
|
|
||||||
peerIDs = append(peerIDs, peer.ID)
|
|
||||||
peer.MarkLoginExpired(true)
|
|
||||||
account.UpdatePeer(peer)
|
|
||||||
err = am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed saving peer status while expiring peer %s", peer.ID)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
am.peersUpdateManager.CloseChannels(peerIDs)
|
|
||||||
err = am.updateAccountPeers(account)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed updating account peers while expiring peers of a blocked user %s", accountID)
|
|
||||||
return nil, err
|
|
||||||
|
|
||||||
|
if err := am.expireAndUpdatePeers(account, blockedPeers); err != nil {
|
||||||
|
log.Errorf("failed update expired peers: %s", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,9 +676,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := am.updateAccountPeers(account); err != nil {
|
am.updateAccountPeers(account)
|
||||||
log.Errorf("failed updating account peers while updating user %s", accountID)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if err = am.Store.SaveAccount(account); err != nil {
|
if err = am.Store.SaveAccount(account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -814,6 +848,68 @@ func (am *DefaultAccountManager) GetUsersFromAccount(accountID, userID string) (
|
|||||||
return userInfos, nil
|
return userInfos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expireAndUpdatePeers expires all peers of the given user and updates them in the account
|
||||||
|
func (am *DefaultAccountManager) expireAndUpdatePeers(account *Account, peers []*Peer) error {
|
||||||
|
var peerIDs []string
|
||||||
|
for _, peer := range peers {
|
||||||
|
if peer.Status.LoginExpired {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerIDs = append(peerIDs, peer.ID)
|
||||||
|
peer.MarkLoginExpired(true)
|
||||||
|
account.UpdatePeer(peer)
|
||||||
|
if err := am.Store.SavePeerStatus(account.Id, peer.ID, *peer.Status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
am.storeEvent(
|
||||||
|
peer.UserID, peer.ID, account.Id,
|
||||||
|
activity.PeerLoginExpired, peer.EventMeta(am.GetDNSDomain()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(peerIDs) != 0 {
|
||||||
|
// this will trigger peer disconnect from the management service
|
||||||
|
am.peersUpdateManager.CloseChannels(peerIDs)
|
||||||
|
am.updateAccountPeers(account)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) deleteUserFromIDP(targetUserID, accountID string) error {
|
||||||
|
if am.userDeleteFromIDPEnabled {
|
||||||
|
log.Debugf("user %s deleted from IdP", targetUserID)
|
||||||
|
err := am.idpManager.DeleteUser(targetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user %s from IdP: %s", targetUserID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := am.idpManager.UpdateUserAppMetadata(targetUserID, idp.AppMetadata{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove user %s app metadata in IdP: %s", targetUserID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = am.refreshCache(accountID)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("refresh account (%q) cache: %v", accountID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *DefaultAccountManager) getEmailAndNameOfTargetUser(accountId, initiatorId, targetId string) (string, string, error) {
|
||||||
|
userInfos, err := am.GetUsersFromAccount(accountId, initiatorId)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
for _, ui := range userInfos {
|
||||||
|
if ui.ID == targetId {
|
||||||
|
return ui.Email, ui.Name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("user info not found for user: %s", targetId)
|
||||||
|
}
|
||||||
|
|
||||||
func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserData, bool) {
|
func findUserInIDPUserdata(userID string, userData []*idp.UserData) (*idp.UserData, bool) {
|
||||||
for _, user := range userData {
|
for _, user := range userData {
|
||||||
if user.ID == userID {
|
if user.ID == userID {
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ func TestUser_DeleteUser_ServiceUser(t *testing.T) {
|
|||||||
assert.Nil(t, store.Accounts[mockAccountID].Users[mockServiceUserID])
|
assert.Nil(t, store.Accounts[mockAccountID].Users[mockServiceUserID])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUser_DeleteUser_regularUser(t *testing.T) {
|
func TestUser_DeleteUser_SelfDelete(t *testing.T) {
|
||||||
store := newStore(t)
|
store := newStore(t)
|
||||||
account := newAccountWithId(mockAccountID, mockUserID, "")
|
account := newAccountWithId(mockAccountID, mockUserID, "")
|
||||||
|
|
||||||
@@ -439,8 +439,35 @@ func TestUser_DeleteUser_regularUser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = am.DeleteUser(mockAccountID, mockUserID, mockUserID)
|
err = am.DeleteUser(mockAccountID, mockUserID, mockUserID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("failed to prevent self deletion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
assert.Errorf(t, err, "Regular users can not be deleted (yet)")
|
func TestUser_DeleteUser_regularUser(t *testing.T) {
|
||||||
|
store := newStore(t)
|
||||||
|
account := newAccountWithId(mockAccountID, mockUserID, "")
|
||||||
|
targetId := "user2"
|
||||||
|
account.Users[targetId] = &User{
|
||||||
|
Id: targetId,
|
||||||
|
IsServiceUser: true,
|
||||||
|
ServiceUserName: "user2username",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.SaveAccount(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error when saving account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
am := DefaultAccountManager{
|
||||||
|
Store: store,
|
||||||
|
eventStore: &activity.InMemoryEventStore{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.DeleteUser(mockAccountID, mockUserID, targetId)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultAccountManager_GetUser(t *testing.T) {
|
func TestDefaultAccountManager_GetUser(t *testing.T) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#!/bin/sh
|
|
||||||
# This code is based on the netbird-installer contribution by physk on GitHub.
|
# This code is based on the netbird-installer contribution by physk on GitHub.
|
||||||
# Source: https://github.com/physk/netbird-installer
|
# Source: https://github.com/physk/netbird-installer
|
||||||
set -e
|
set -e
|
||||||
@@ -17,6 +16,12 @@ OS_TYPE=""
|
|||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
PACKAGE_MANAGER="bin"
|
PACKAGE_MANAGER="bin"
|
||||||
INSTALL_DIR=""
|
INSTALL_DIR=""
|
||||||
|
SUDO=""
|
||||||
|
|
||||||
|
|
||||||
|
if command -v sudo > /dev/null && [ "$(id -u)" -ne 0 ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
get_latest_release() {
|
get_latest_release() {
|
||||||
if [ -n "$GITHUB_TOKEN" ]; then
|
if [ -n "$GITHUB_TOKEN" ]; then
|
||||||
@@ -65,27 +70,35 @@ download_release_binary() {
|
|||||||
unzip -q -o "$BINARY_NAME"
|
unzip -q -o "$BINARY_NAME"
|
||||||
mv "netbird_ui_${OS_TYPE}_${ARCH}" "$INSTALL_DIR"
|
mv "netbird_ui_${OS_TYPE}_${ARCH}" "$INSTALL_DIR"
|
||||||
else
|
else
|
||||||
sudo mkdir -p "$INSTALL_DIR"
|
${SUDO} mkdir -p "$INSTALL_DIR"
|
||||||
tar -xzvf "$BINARY_NAME"
|
tar -xzvf "$BINARY_NAME"
|
||||||
sudo mv "${1%_"${BINARY_BASE_NAME}"}" "$INSTALL_DIR/"
|
${SUDO} mv "${1%_"${BINARY_BASE_NAME}"}" "$INSTALL_DIR/"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
add_apt_repo() {
|
add_apt_repo() {
|
||||||
sudo apt-get update
|
${SUDO} apt-get update
|
||||||
sudo apt-get install ca-certificates gnupg -y
|
${SUDO} apt-get install ca-certificates curl gnupg -y
|
||||||
|
|
||||||
curl -sSL https://pkgs.wiretrustee.com/debian/public.key \
|
# Remove old keys and repo source files
|
||||||
| sudo gpg --dearmor --output /usr/share/keyrings/wiretrustee-archive-keyring.gpg
|
${SUDO} rm -f \
|
||||||
|
/etc/apt/sources.list.d/netbird.list \
|
||||||
|
/etc/apt/sources.list.d/wiretrustee.list \
|
||||||
|
/etc/apt/trusted.gpg.d/wiretrustee.gpg \
|
||||||
|
/usr/share/keyrings/netbird-archive-keyring.gpg \
|
||||||
|
/usr/share/keyrings/wiretrustee-archive-keyring.gpg
|
||||||
|
|
||||||
APT_REPO="deb [signed-by=/usr/share/keyrings/wiretrustee-archive-keyring.gpg] https://pkgs.wiretrustee.com/debian stable main"
|
curl -sSL https://pkgs.netbird.io/debian/public.key \
|
||||||
echo "$APT_REPO" | sudo tee /etc/apt/sources.list.d/wiretrustee.list
|
| ${SUDO} gpg --dearmor -o /usr/share/keyrings/netbird-archive-keyring.gpg
|
||||||
|
|
||||||
sudo apt-get update
|
echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' \
|
||||||
|
| ${SUDO} tee /etc/apt/sources.list.d/netbird.list
|
||||||
|
|
||||||
|
${SUDO} apt-get update
|
||||||
}
|
}
|
||||||
|
|
||||||
add_rpm_repo() {
|
add_rpm_repo() {
|
||||||
cat <<-EOF | sudo tee /etc/yum.repos.d/netbird.repo
|
cat <<-EOF | ${SUDO} tee /etc/yum.repos.d/netbird.repo
|
||||||
[NetBird]
|
[NetBird]
|
||||||
name=NetBird
|
name=NetBird
|
||||||
baseurl=https://pkgs.netbird.io/yum/
|
baseurl=https://pkgs.netbird.io/yum/
|
||||||
@@ -104,7 +117,7 @@ add_aur_repo() {
|
|||||||
for PKG in $INSTALL_PKGS; do
|
for PKG in $INSTALL_PKGS; do
|
||||||
if ! pacman -Q "$PKG" > /dev/null 2>&1; then
|
if ! pacman -Q "$PKG" > /dev/null 2>&1; then
|
||||||
# Install missing package(s)
|
# Install missing package(s)
|
||||||
sudo pacman -S "$PKG" --noconfirm
|
${SUDO} pacman -S "$PKG" --noconfirm
|
||||||
|
|
||||||
# Add installed package for clean up later
|
# Add installed package for clean up later
|
||||||
REMOVE_PKGS="$REMOVE_PKGS $PKG"
|
REMOVE_PKGS="$REMOVE_PKGS $PKG"
|
||||||
@@ -121,7 +134,7 @@ add_aur_repo() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up the installed packages
|
# Clean up the installed packages
|
||||||
sudo pacman -Rs "$REMOVE_PKGS" --noconfirm
|
${SUDO} pacman -Rs "$REMOVE_PKGS" --noconfirm
|
||||||
}
|
}
|
||||||
|
|
||||||
install_native_binaries() {
|
install_native_binaries() {
|
||||||
@@ -181,8 +194,136 @@ install_netbird() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Identify OS name and default package manager
|
# Run the installation, if a desktop environment is not detected
|
||||||
if type uname >/dev/null 2>&1; then
|
# only the CLI will be installed
|
||||||
|
case "$PACKAGE_MANAGER" in
|
||||||
|
apt)
|
||||||
|
add_apt_repo
|
||||||
|
${SUDO} apt-get install netbird -y
|
||||||
|
|
||||||
|
if ! $SKIP_UI_APP; then
|
||||||
|
${SUDO} apt-get install netbird-ui -y
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
yum)
|
||||||
|
add_rpm_repo
|
||||||
|
${SUDO} yum -y install netbird
|
||||||
|
if ! $SKIP_UI_APP; then
|
||||||
|
${SUDO} yum -y install netbird-ui
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dnf)
|
||||||
|
add_rpm_repo
|
||||||
|
${SUDO} dnf -y install dnf-plugin-config-manager
|
||||||
|
${SUDO} dnf config-manager --add-repo /etc/yum.repos.d/netbird.repo
|
||||||
|
${SUDO} dnf -y install netbird
|
||||||
|
|
||||||
|
if ! $SKIP_UI_APP; then
|
||||||
|
${SUDO} dnf -y install netbird-ui
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
pacman)
|
||||||
|
${SUDO} pacman -Syy
|
||||||
|
add_aur_repo
|
||||||
|
;;
|
||||||
|
brew)
|
||||||
|
# Remove Wiretrustee if it had been installed using Homebrew before
|
||||||
|
if brew ls --versions wiretrustee >/dev/null 2>&1; then
|
||||||
|
echo "Removing existing wiretrustee client"
|
||||||
|
|
||||||
|
# Stop and uninstall daemon service:
|
||||||
|
wiretrustee service stop
|
||||||
|
wiretrustee service uninstall
|
||||||
|
|
||||||
|
# Unlik the app
|
||||||
|
brew unlink wiretrustee
|
||||||
|
fi
|
||||||
|
|
||||||
|
brew install netbirdio/tap/netbird
|
||||||
|
if ! $SKIP_UI_APP; then
|
||||||
|
brew install --cask netbirdio/tap/netbird-ui
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ "$OS_NAME" = "nixos" ];then
|
||||||
|
echo "Please add NetBird to your NixOS configuration.nix directly:"
|
||||||
|
echo ""
|
||||||
|
echo "services.netbird.enable = true;"
|
||||||
|
|
||||||
|
if ! $SKIP_UI_APP; then
|
||||||
|
echo "environment.systemPackages = [ pkgs.netbird-ui ];"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build and apply new configuration:"
|
||||||
|
echo ""
|
||||||
|
echo "${SUDO} nixos-rebuild switch"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_native_binaries
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Add package manager to config
|
||||||
|
${SUDO} mkdir -p "$CONFIG_FOLDER"
|
||||||
|
echo "package_manager=$PACKAGE_MANAGER" | ${SUDO} tee "$CONFIG_FILE" > /dev/null
|
||||||
|
|
||||||
|
# Load and start netbird service
|
||||||
|
if ! ${SUDO} netbird service install 2>&1; then
|
||||||
|
echo "NetBird service has already been loaded"
|
||||||
|
fi
|
||||||
|
if ! ${SUDO} netbird service start 2>&1; then
|
||||||
|
echo "NetBird service has already been started"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo "Installation has been finished. To connect, you need to run NetBird by executing the following command:"
|
||||||
|
echo ""
|
||||||
|
echo "netbird up"
|
||||||
|
}
|
||||||
|
|
||||||
|
version_greater_equal() {
|
||||||
|
printf '%s\n%s\n' "$2" "$1" | sort -V -C
|
||||||
|
}
|
||||||
|
|
||||||
|
is_bin_package_manager() {
|
||||||
|
if ${SUDO} test -f "$1" && ${SUDO} grep -q "package_manager=bin" "$1" ; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_netbird() {
|
||||||
|
if is_bin_package_manager "$CONFIG_FILE"; then
|
||||||
|
latest_release=$(get_latest_release)
|
||||||
|
latest_version=${latest_release#v}
|
||||||
|
installed_version=$(netbird version)
|
||||||
|
|
||||||
|
if [ "$latest_version" = "$installed_version" ]; then
|
||||||
|
echo "Installed netbird version ($installed_version) is up-to-date"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if version_greater_equal "$latest_version" "$installed_version"; then
|
||||||
|
echo "NetBird new version ($latest_version) available. Updating..."
|
||||||
|
echo ""
|
||||||
|
echo "Initiating NetBird update. This will stop the netbird service and restart it after the update"
|
||||||
|
|
||||||
|
${SUDO} netbird service stop
|
||||||
|
${SUDO} netbird service uninstall
|
||||||
|
install_native_binaries
|
||||||
|
|
||||||
|
${SUDO} netbird service install
|
||||||
|
${SUDO} netbird service start
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "NetBird installation was done using a package manager. Please use your system's package manager to update"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Identify OS name and default package manager
|
||||||
|
if type uname >/dev/null 2>&1; then
|
||||||
case "$(uname)" in
|
case "$(uname)" in
|
||||||
Linux)
|
Linux)
|
||||||
OS_NAME="$(. /etc/os-release && echo "$ID")"
|
OS_NAME="$(. /etc/os-release && echo "$ID")"
|
||||||
@@ -233,136 +374,7 @@ install_netbird() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the installation, if a desktop environment is not detected
|
|
||||||
# only the CLI will be installed
|
|
||||||
case "$PACKAGE_MANAGER" in
|
|
||||||
apt)
|
|
||||||
add_apt_repo
|
|
||||||
sudo apt-get install netbird -y
|
|
||||||
|
|
||||||
if ! $SKIP_UI_APP; then
|
|
||||||
sudo apt-get install netbird-ui -y
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
yum)
|
|
||||||
add_rpm_repo
|
|
||||||
sudo yum -y install netbird
|
|
||||||
if ! $SKIP_UI_APP; then
|
|
||||||
sudo yum -y install netbird-ui
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
dnf)
|
|
||||||
add_rpm_repo
|
|
||||||
sudo dnf -y install dnf-plugin-config-manager
|
|
||||||
sudo dnf config-manager --add-repo /etc/yum.repos.d/netbird.repo
|
|
||||||
sudo dnf -y install netbird
|
|
||||||
|
|
||||||
if ! $SKIP_UI_APP; then
|
|
||||||
sudo dnf -y install netbird-ui
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
pacman)
|
|
||||||
sudo pacman -Syy
|
|
||||||
add_aur_repo
|
|
||||||
;;
|
|
||||||
brew)
|
|
||||||
# Remove Wiretrustee if it had been installed using Homebrew before
|
|
||||||
if brew ls --versions wiretrustee >/dev/null 2>&1; then
|
|
||||||
echo "Removing existing wiretrustee client"
|
|
||||||
|
|
||||||
# Stop and uninstall daemon service:
|
|
||||||
wiretrustee service stop
|
|
||||||
wiretrustee service uninstall
|
|
||||||
|
|
||||||
# Unlik the app
|
|
||||||
brew unlink wiretrustee
|
|
||||||
fi
|
|
||||||
|
|
||||||
brew install netbirdio/tap/netbird
|
|
||||||
if ! $SKIP_UI_APP; then
|
|
||||||
brew install --cask netbirdio/tap/netbird-ui
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [ "$OS_NAME" = "nixos" ];then
|
|
||||||
echo "Please add NetBird to your NixOS configuration.nix directly:"
|
|
||||||
echo ""
|
|
||||||
echo "services.netbird.enable = true;"
|
|
||||||
|
|
||||||
if ! $SKIP_UI_APP; then
|
|
||||||
echo "environment.systemPackages = [ pkgs.netbird-ui ];"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Build and apply new configuration:"
|
|
||||||
echo ""
|
|
||||||
echo "sudo nixos-rebuild switch"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
install_native_binaries
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Add package manager to config
|
|
||||||
sudo mkdir -p "$CONFIG_FOLDER"
|
|
||||||
echo "package_manager=$PACKAGE_MANAGER" | sudo tee "$CONFIG_FILE" > /dev/null
|
|
||||||
|
|
||||||
# Load and start netbird service
|
|
||||||
if ! sudo netbird service install 2>&1; then
|
|
||||||
echo "NetBird service has already been loaded"
|
|
||||||
fi
|
|
||||||
if ! sudo netbird service start 2>&1; then
|
|
||||||
echo "NetBird service has already been started"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
echo "Installation has been finished. To connect, you need to run NetBird by executing the following command:"
|
|
||||||
echo ""
|
|
||||||
echo "sudo netbird up"
|
|
||||||
}
|
|
||||||
|
|
||||||
version_greater_equal() {
|
|
||||||
printf '%s\n%s\n' "$2" "$1" | sort -V -C
|
|
||||||
}
|
|
||||||
|
|
||||||
is_bin_package_manager() {
|
|
||||||
if sudo test -f "$1" && sudo grep -q "package_manager=bin" "$1" ; then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_netbird() {
|
|
||||||
if is_bin_package_manager "$CONFIG_FILE"; then
|
|
||||||
latest_release=$(get_latest_release)
|
|
||||||
latest_version=${latest_release#v}
|
|
||||||
installed_version=$(netbird version)
|
|
||||||
|
|
||||||
if [ "$latest_version" = "$installed_version" ]; then
|
|
||||||
echo "Installed netbird version ($installed_version) is up-to-date"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if version_greater_equal "$latest_version" "$installed_version"; then
|
|
||||||
echo "NetBird new version ($latest_version) available. Updating..."
|
|
||||||
echo ""
|
|
||||||
echo "Initiating NetBird update. This will stop the netbird service and restart it after the update"
|
|
||||||
|
|
||||||
sudo netbird service stop
|
|
||||||
sudo netbird service uninstall
|
|
||||||
install_native_binaries
|
|
||||||
|
|
||||||
sudo netbird service install
|
|
||||||
sudo netbird service start
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "NetBird installation was done using a package manager. Please use your system's package manager to update"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--update)
|
--update)
|
||||||
@@ -370,4 +382,4 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
install_netbird
|
install_netbird
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ type Route struct {
|
|||||||
NetID string
|
NetID string
|
||||||
Description string
|
Description string
|
||||||
Peer string
|
Peer string
|
||||||
|
PeerGroups []string
|
||||||
NetworkType NetworkType
|
NetworkType NetworkType
|
||||||
Masquerade bool
|
Masquerade bool
|
||||||
Metric int
|
Metric int
|
||||||
@@ -79,7 +80,7 @@ type Route struct {
|
|||||||
|
|
||||||
// EventMeta returns activity event meta related to the route
|
// EventMeta returns activity event meta related to the route
|
||||||
func (r *Route) EventMeta() map[string]any {
|
func (r *Route) EventMeta() map[string]any {
|
||||||
return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "peer_id": r.Peer}
|
return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "peer_id": r.Peer, "peer_groups": r.PeerGroups}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy copies a route object
|
// Copy copies a route object
|
||||||
@@ -91,12 +92,14 @@ func (r *Route) Copy() *Route {
|
|||||||
Network: r.Network,
|
Network: r.Network,
|
||||||
NetworkType: r.NetworkType,
|
NetworkType: r.NetworkType,
|
||||||
Peer: r.Peer,
|
Peer: r.Peer,
|
||||||
|
PeerGroups: make([]string, len(r.PeerGroups)),
|
||||||
Metric: r.Metric,
|
Metric: r.Metric,
|
||||||
Masquerade: r.Masquerade,
|
Masquerade: r.Masquerade,
|
||||||
Enabled: r.Enabled,
|
Enabled: r.Enabled,
|
||||||
Groups: make([]string, len(r.Groups)),
|
Groups: make([]string, len(r.Groups)),
|
||||||
}
|
}
|
||||||
copy(route.Groups, r.Groups)
|
copy(route.Groups, r.Groups)
|
||||||
|
copy(route.PeerGroups, r.PeerGroups)
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +114,8 @@ func (r *Route) IsEqual(other *Route) bool {
|
|||||||
other.Metric == r.Metric &&
|
other.Metric == r.Metric &&
|
||||||
other.Masquerade == r.Masquerade &&
|
other.Masquerade == r.Masquerade &&
|
||||||
other.Enabled == r.Enabled &&
|
other.Enabled == r.Enabled &&
|
||||||
compareGroupsList(r.Groups, other.Groups)
|
compareList(r.Groups, other.Groups) &&
|
||||||
|
compareList(r.PeerGroups, other.PeerGroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6
|
// ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6
|
||||||
@@ -134,7 +138,7 @@ func ParseNetwork(networkString string) (NetworkType, netip.Prefix, error) {
|
|||||||
return IPv4Network, masked, nil
|
return IPv4Network, masked, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compareGroupsList(list, other []string) bool {
|
func compareList(list, other []string) bool {
|
||||||
if len(list) != len(other) {
|
if len(list) != len(other) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user