mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-23 16:43:29 +01:00
e3d2b6a408
The new functionality allows blocking a user in the Management service. Blocked users lose access to the Dashboard, aren't able to modify the network map, and all of their connected devices disconnect and are set to the "login expired" state. Technically all above was achieved with the updated PUT /api/users endpoint, that was extended with the is_blocked field.
395 lines
12 KiB
Go
395 lines
12 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/netbirdio/netbird/management/server"
|
|
"github.com/netbirdio/netbird/management/server/http/api"
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
|
"github.com/netbirdio/netbird/management/server/mock_server"
|
|
"github.com/netbirdio/netbird/management/server/status"
|
|
)
|
|
|
|
const (
|
|
serviceUserID = "serviceUserID"
|
|
regularUserID = "regularUserID"
|
|
)
|
|
|
|
var usersTestAccount = &server.Account{
|
|
Id: existingAccountID,
|
|
Domain: domain,
|
|
Users: map[string]*server.User{
|
|
existingUserID: {
|
|
Id: existingUserID,
|
|
Role: "admin",
|
|
IsServiceUser: false,
|
|
AutoGroups: []string{"group_1"},
|
|
},
|
|
regularUserID: {
|
|
Id: regularUserID,
|
|
Role: "user",
|
|
IsServiceUser: false,
|
|
AutoGroups: []string{"group_1"},
|
|
},
|
|
serviceUserID: {
|
|
Id: serviceUserID,
|
|
Role: "user",
|
|
IsServiceUser: true,
|
|
AutoGroups: []string{"group_1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
func initUsersTestData() *UsersHandler {
|
|
return &UsersHandler{
|
|
accountManager: &mock_server.MockAccountManager{
|
|
GetAccountFromTokenFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
|
return usersTestAccount, usersTestAccount.Users[claims.UserId], nil
|
|
},
|
|
GetUsersFromAccountFunc: func(accountID, userID string) ([]*server.UserInfo, error) {
|
|
users := make([]*server.UserInfo, 0)
|
|
for _, v := range usersTestAccount.Users {
|
|
users = append(users, &server.UserInfo{
|
|
ID: v.Id,
|
|
Role: string(v.Role),
|
|
Name: "",
|
|
Email: "",
|
|
IsServiceUser: v.IsServiceUser,
|
|
})
|
|
}
|
|
return users, nil
|
|
},
|
|
CreateUserFunc: func(accountID, userID string, key *server.UserInfo) (*server.UserInfo, error) {
|
|
if userID != existingUserID {
|
|
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
|
|
}
|
|
return key, nil
|
|
},
|
|
DeleteUserFunc: func(accountID string, initiatorUserID string, targetUserID string) error {
|
|
if targetUserID == notFoundUserID {
|
|
return status.Errorf(status.NotFound, "user with ID %s does not exists", targetUserID)
|
|
}
|
|
if !usersTestAccount.Users[targetUserID].IsServiceUser {
|
|
return status.Errorf(status.PermissionDenied, "user with ID %s is not a service user and can not be deleted", targetUserID)
|
|
}
|
|
return nil
|
|
},
|
|
SaveUserFunc: func(accountID, userID string, update *server.User) (*server.UserInfo, error) {
|
|
if update.Id == notFoundUserID {
|
|
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", update.Id)
|
|
}
|
|
|
|
if userID != existingUserID {
|
|
return nil, status.Errorf(status.NotFound, "user with ID %s does not exists", userID)
|
|
}
|
|
|
|
info, err := update.Copy().ToUserInfo(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return info, nil
|
|
},
|
|
},
|
|
claimsExtractor: jwtclaims.NewClaimsExtractor(
|
|
jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims {
|
|
return jwtclaims.AuthorizationClaims{
|
|
UserId: existingUserID,
|
|
Domain: domain,
|
|
AccountId: existingAccountID,
|
|
}
|
|
}),
|
|
),
|
|
}
|
|
}
|
|
|
|
func TestGetUsers(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
requestType string
|
|
requestPath string
|
|
expectedUserIDs []string
|
|
}{
|
|
{name: "GetAllUsers", requestType: http.MethodGet, requestPath: "/api/users", expectedStatus: http.StatusOK, expectedUserIDs: []string{existingUserID, regularUserID, serviceUserID}},
|
|
{name: "GetOnlyServiceUsers", requestType: http.MethodGet, requestPath: "/api/users?service_user=true", expectedStatus: http.StatusOK, expectedUserIDs: []string{serviceUserID}},
|
|
{name: "GetOnlyRegularUsers", requestType: http.MethodGet, requestPath: "/api/users?service_user=false", expectedStatus: http.StatusOK, expectedUserIDs: []string{existingUserID, regularUserID}},
|
|
}
|
|
|
|
userHandler := initUsersTestData()
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
|
|
|
userHandler.GetAllUsers(recorder, req)
|
|
|
|
res := recorder.Result()
|
|
defer res.Body.Close()
|
|
|
|
content, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatalf("I don't know what I expected; %v", err)
|
|
}
|
|
|
|
if status := recorder.Code; status != tc.expectedStatus {
|
|
t.Errorf("handler returned wrong status code: got %v want %v, content: %s",
|
|
status, tc.expectedStatus, string(content))
|
|
return
|
|
}
|
|
|
|
respBody := []*server.UserInfo{}
|
|
err = json.Unmarshal(content, &respBody)
|
|
if err != nil {
|
|
t.Fatalf("Sent content is not in correct json format; %v", err)
|
|
}
|
|
|
|
assert.Equal(t, len(respBody), len(tc.expectedUserIDs))
|
|
for _, v := range respBody {
|
|
assert.Contains(t, tc.expectedUserIDs, v.ID)
|
|
assert.Equal(t, v.ID, usersTestAccount.Users[v.ID].Id)
|
|
assert.Equal(t, v.Role, string(usersTestAccount.Users[v.ID].Role))
|
|
assert.Equal(t, v.IsServiceUser, usersTestAccount.Users[v.ID].IsServiceUser)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateUser(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
expectedStatusCode int
|
|
requestType string
|
|
requestPath string
|
|
requestBody io.Reader
|
|
expectedUserID string
|
|
expectedRole string
|
|
expectedStatus string
|
|
expectedBlocked bool
|
|
expectedIsServiceUser bool
|
|
expectedGroups []string
|
|
}{
|
|
{
|
|
name: "Update_Block_User",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/users/" + regularUserID,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedUserID: regularUserID,
|
|
expectedBlocked: true,
|
|
expectedRole: "user",
|
|
expectedStatus: "blocked",
|
|
expectedGroups: []string{"group_1"},
|
|
requestBody: bytes.NewBufferString("{\"role\":\"user\",\"auto_groups\":[\"group_1\"],\"is_service_user\":false, \"is_blocked\": true}"),
|
|
},
|
|
{
|
|
name: "Update_Change_Role_To_Admin",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/users/" + regularUserID,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedUserID: regularUserID,
|
|
expectedBlocked: false,
|
|
expectedRole: "admin",
|
|
expectedStatus: "blocked",
|
|
expectedGroups: []string{"group_1"},
|
|
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"auto_groups\":[\"group_1\"],\"is_service_user\":false, \"is_blocked\": false}"),
|
|
},
|
|
{
|
|
name: "Update_Groups",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/users/" + regularUserID,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedUserID: regularUserID,
|
|
expectedBlocked: false,
|
|
expectedRole: "admin",
|
|
expectedStatus: "blocked",
|
|
expectedGroups: []string{"group_2", "group_3"},
|
|
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"auto_groups\":[\"group_3\", \"group_2\"],\"is_service_user\":false, \"is_blocked\": false}"),
|
|
},
|
|
{
|
|
name: "Should_Fail_Because_AutoGroups_Is_Absent",
|
|
requestType: http.MethodPut,
|
|
requestPath: "/api/users/" + regularUserID,
|
|
expectedStatusCode: http.StatusBadRequest,
|
|
expectedUserID: regularUserID,
|
|
expectedBlocked: false,
|
|
expectedRole: "admin",
|
|
expectedStatus: "blocked",
|
|
expectedGroups: []string{"group_2", "group_3"},
|
|
requestBody: bytes.NewBufferString("{\"role\":\"admin\",\"is_service_user\":false, \"is_blocked\": false}"),
|
|
},
|
|
}
|
|
|
|
userHandler := initUsersTestData()
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
|
|
|
router := mux.NewRouter()
|
|
router.HandleFunc("/api/users/{userId}", userHandler.UpdateUser).Methods("PUT")
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
res := recorder.Result()
|
|
defer res.Body.Close()
|
|
|
|
if status := recorder.Code; status != tc.expectedStatusCode {
|
|
t.Fatalf("handler returned wrong status code: got %v want %v",
|
|
status, http.StatusOK)
|
|
}
|
|
|
|
if tc.expectedStatusCode == 200 {
|
|
|
|
content, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatalf("I don't know what I expected; %v", err)
|
|
}
|
|
|
|
respBody := &api.User{}
|
|
err = json.Unmarshal(content, &respBody)
|
|
if err != nil {
|
|
t.Fatalf("response content is not in correct json format; %v", err)
|
|
}
|
|
|
|
assert.Equal(t, tc.expectedUserID, respBody.Id)
|
|
assert.Equal(t, tc.expectedRole, respBody.Role)
|
|
assert.Equal(t, tc.expectedIsServiceUser, *respBody.IsServiceUser)
|
|
assert.Equal(t, tc.expectedBlocked, respBody.IsBlocked)
|
|
assert.Len(t, respBody.AutoGroups, len(tc.expectedGroups))
|
|
|
|
for _, expectedGroup := range tc.expectedGroups {
|
|
exists := false
|
|
for _, actualGroup := range respBody.AutoGroups {
|
|
if expectedGroup == actualGroup {
|
|
exists = true
|
|
}
|
|
}
|
|
assert.True(t, exists, fmt.Sprintf("group %s not found in the response", expectedGroup))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateUser(t *testing.T) {
|
|
name := "name"
|
|
email := "email"
|
|
serviceUserToAdd := api.UserCreateRequest{
|
|
AutoGroups: []string{},
|
|
Email: nil,
|
|
IsServiceUser: true,
|
|
Name: &name,
|
|
Role: "admin",
|
|
}
|
|
serviceUserString, err := json.Marshal(serviceUserToAdd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
regularUserToAdd := api.UserCreateRequest{
|
|
AutoGroups: []string{},
|
|
Email: &email,
|
|
IsServiceUser: true,
|
|
Name: &name,
|
|
Role: "admin",
|
|
}
|
|
regularUserString, err := json.Marshal(regularUserToAdd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
requestType string
|
|
requestPath string
|
|
requestBody io.Reader
|
|
expectedResult []*server.User
|
|
}{
|
|
{name: "CreateServiceUser", requestType: http.MethodPost, requestPath: "/api/users", expectedStatus: http.StatusOK, requestBody: bytes.NewBuffer(serviceUserString)},
|
|
// right now creation is blocked in AC middleware, will be refactored in the future
|
|
{name: "CreateRegularUser", requestType: http.MethodPost, requestPath: "/api/users", expectedStatus: http.StatusOK, requestBody: bytes.NewBuffer(regularUserString)},
|
|
}
|
|
|
|
userHandler := initUsersTestData()
|
|
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
|
|
rr := httptest.NewRecorder()
|
|
|
|
userHandler.CreateUser(rr, req)
|
|
|
|
res := rr.Result()
|
|
defer res.Body.Close()
|
|
|
|
if status := rr.Code; status != tc.expectedStatus {
|
|
t.Fatalf("handler returned wrong status code: got %v want %v",
|
|
status, tc.expectedStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteUser(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
expectedStatus int
|
|
expectedBody bool
|
|
requestType string
|
|
requestPath string
|
|
requestVars map[string]string
|
|
requestBody io.Reader
|
|
}{
|
|
{
|
|
name: "Delete Regular User",
|
|
requestType: http.MethodDelete,
|
|
requestPath: "/api/users/" + regularUserID,
|
|
requestVars: map[string]string{"userId": regularUserID},
|
|
expectedStatus: http.StatusForbidden,
|
|
},
|
|
{
|
|
name: "Delete Service User",
|
|
requestType: http.MethodDelete,
|
|
requestPath: "/api/users/" + serviceUserID,
|
|
requestVars: map[string]string{"userId": serviceUserID},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "Delete Not Existing User",
|
|
requestType: http.MethodDelete,
|
|
requestPath: "/api/users/" + notFoundUserID,
|
|
requestVars: map[string]string{"userId": notFoundUserID},
|
|
expectedStatus: http.StatusNotFound,
|
|
},
|
|
}
|
|
|
|
userHandler := initUsersTestData()
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.requestType, tc.requestPath, nil)
|
|
req = mux.SetURLVars(req, tc.requestVars)
|
|
rr := httptest.NewRecorder()
|
|
|
|
userHandler.DeleteUser(rr, req)
|
|
|
|
res := rr.Result()
|
|
defer res.Body.Close()
|
|
|
|
if status := rr.Code; status != tc.expectedStatus {
|
|
t.Fatalf("handler returned wrong status code: got %v want %v",
|
|
status, tc.expectedStatus)
|
|
}
|
|
})
|
|
}
|
|
}
|