SCIM integration tests (#27750)
For #27287 This PR adds integration tests for SCIM API endpoints as well as some bug fixes found by these tests. # Checklist for submitter - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -191,7 +191,7 @@ func TestCreateBulkUsers(t *testing.T) {
|
||||
user15,user15@example.com,false,false,,1:admin
|
||||
user16,user16@example.com,false,false,,1:admin 2:maintainer`)
|
||||
|
||||
expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"observer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}}
|
||||
expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"maintainer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}}
|
||||
`
|
||||
|
||||
assert.Equal(t, "", runAppForTest(t, []string{"user", "create-users", "--csv", csvFile}))
|
||||
|
||||
@@ -18,7 +18,7 @@ Create a SAML app in an IdP.
|
||||
|
||||
## Description
|
||||
|
||||
If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/api/v1/fleet/mdm/sso`. This page initiates the SSO flow in the setup assistant webview.
|
||||
If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/mdm/sso`. This page gets the SAML Request from `{server_url}/api/v1/fleet/mdm/sso` and initiates the SSO flow in the setup assistant web view.
|
||||
|
||||
`end_user_authentication` setting is global, but `enable_end_user_authentication` is a team setting.
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
|
||||
- https://developer.okta.com/docs/guides/scim-provisioning-integration-prepare/main/
|
||||
|
||||
Sample provisioning settings that work. Capabilities can be disabled and attributes can be removed as needed.
|
||||
|
||||

|
||||
|
||||
### Testing Okta integration
|
||||
|
||||
First, create at least one SCIM user:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
package scim
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
|
||||
"github.com/go-kit/log"
|
||||
)
|
||||
|
||||
type Suite struct {
|
||||
integrationtest.BaseSuite
|
||||
}
|
||||
|
||||
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
||||
ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName)
|
||||
logger := log.NewLogfmtLogger(os.Stdout)
|
||||
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierFree,
|
||||
},
|
||||
FleetConfig: &fleetCfg,
|
||||
Pool: redisPool,
|
||||
Logger: logger,
|
||||
EnableSCIM: true,
|
||||
})
|
||||
|
||||
s := &Suite{
|
||||
BaseSuite: integrationtest.BaseSuite{
|
||||
Logger: logger,
|
||||
DS: ds,
|
||||
FleetCfg: fleetCfg,
|
||||
Users: users,
|
||||
Server: server,
|
||||
},
|
||||
}
|
||||
|
||||
integrationtest.SetUpServerURL(t, ds, server)
|
||||
|
||||
s.Token = s.GetTestAdminToken(t)
|
||||
return s
|
||||
}
|
||||
@@ -155,10 +155,12 @@ func createGroupResource(group *fleet.ScimGroup) scim.Resource {
|
||||
return groupResource
|
||||
}
|
||||
|
||||
// GetAll
|
||||
// Pagination is 1-indexed.
|
||||
func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
|
||||
page := params.StartIndex
|
||||
if page < 1 {
|
||||
page = 1
|
||||
startIndex := params.StartIndex
|
||||
if startIndex < 1 {
|
||||
startIndex = 1
|
||||
}
|
||||
count := params.Count
|
||||
if count > maxResults {
|
||||
@@ -169,8 +171,8 @@ func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (s
|
||||
}
|
||||
|
||||
opts := fleet.ScimListOptions{
|
||||
Page: uint(page), // nolint:gosec // ignore G115
|
||||
PerPage: uint(count), // nolint:gosec // ignore G115
|
||||
StartIndex: uint(startIndex), // nolint:gosec // ignore G115
|
||||
PerPage: uint(count), // nolint:gosec // ignore G115
|
||||
}
|
||||
|
||||
resourceFilter := r.URL.Query().Get("filter")
|
||||
|
||||
+26
-13
@@ -50,6 +50,11 @@ func (u *UserHandler) Create(r *http.Request, attributes scim.ResourceAttributes
|
||||
level.Error(u.logger).Log("msg", "failed to get userName", "err", err)
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
// In IETF documents, “non-empty” is generally used in the literal sense of “having at least one character.” That means if a value contains one or more spaces (and nothing else), it is still considered non-empty.
|
||||
if len(userName) == 0 {
|
||||
level.Info(u.logger).Log("msg", "userName is empty")
|
||||
return scim.Resource{}, errors.ScimErrorBadParams([]string{userNameAttr})
|
||||
}
|
||||
_, err = u.ds.ScimUserByUserName(r.Context(), userName)
|
||||
switch {
|
||||
case err != nil && !fleet.IsNotFound(err):
|
||||
@@ -260,6 +265,8 @@ func createUserResource(user *fleet.ScimUser) scim.Resource {
|
||||
}
|
||||
|
||||
// GetAll
|
||||
// Pagination is 1-indexed.
|
||||
//
|
||||
// Per RFC7644 3.4.2, SHOULD ignore any query parameters they do not recognize instead of rejecting the query for versioning compatibility reasons
|
||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2
|
||||
//
|
||||
@@ -274,9 +281,9 @@ func createUserResource(user *fleet.ScimUser) scim.Resource {
|
||||
// totalResults: The total number of results returned by the list or query operation. The value may be larger than the number of
|
||||
// resources returned, such as when returning a single page (see Section 3.4.2.4) of results where multiple pages are available.
|
||||
func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) {
|
||||
page := params.StartIndex
|
||||
if page < 1 {
|
||||
page = 1
|
||||
startIndex := params.StartIndex
|
||||
if startIndex < 1 {
|
||||
startIndex = 1
|
||||
}
|
||||
count := params.Count
|
||||
if count > maxResults {
|
||||
@@ -288,8 +295,8 @@ func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (sc
|
||||
|
||||
opts := fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: uint(page), // nolint:gosec // ignore G115
|
||||
PerPage: uint(count), // nolint:gosec // ignore G115
|
||||
StartIndex: uint(startIndex), // nolint:gosec // ignore G115
|
||||
PerPage: uint(count), // nolint:gosec // ignore G115
|
||||
},
|
||||
}
|
||||
resourceFilter := r.URL.Query().Get("filter")
|
||||
@@ -400,6 +407,10 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
|
||||
if len(operations) > 1 {
|
||||
level.Info(u.logger).Log("msg", "too many patch operations")
|
||||
return scim.Resource{}, errors.ScimErrorBadParams([]string{"Operations"})
|
||||
}
|
||||
for _, op := range operations {
|
||||
if op.Op != "replace" {
|
||||
level.Info(u.logger).Log("msg", "unsupported patch operation", "op", op.Op)
|
||||
@@ -435,14 +446,16 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO
|
||||
}
|
||||
}
|
||||
|
||||
err = u.ds.ReplaceScimUser(r.Context(), user)
|
||||
switch {
|
||||
case fleet.IsNotFound(err):
|
||||
level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id)
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
case err != nil:
|
||||
level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err)
|
||||
return scim.Resource{}, err
|
||||
if len(operations) != 0 {
|
||||
err = u.ds.ReplaceScimUser(r.Context(), user)
|
||||
switch {
|
||||
case fleet.IsNotFound(err):
|
||||
level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id)
|
||||
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
|
||||
case err != nil:
|
||||
level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err)
|
||||
return scim.Resource{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return createUserResource(user), nil
|
||||
|
||||
@@ -227,13 +227,6 @@ func insertEmails(ctx context.Context, tx sqlx.ExtContext, user *fleet.ScimUser)
|
||||
// DeleteScimUser deletes a SCIM user from the database
|
||||
func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error {
|
||||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
// Delete all email entries for the user
|
||||
const deleteEmailsQuery = `DELETE FROM scim_user_emails WHERE scim_user_id = ?`
|
||||
_, err := tx.ExecContext(ctx, deleteEmailsQuery, id)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "delete scim user emails")
|
||||
}
|
||||
|
||||
// Delete the user
|
||||
const deleteUserQuery = `DELETE FROM scim_users WHERE id = ?`
|
||||
result, err := tx.ExecContext(ctx, deleteUserQuery, id)
|
||||
@@ -257,16 +250,13 @@ func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error {
|
||||
// ListScimUsers retrieves a list of SCIM users with optional filtering
|
||||
func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersListOptions) (users []fleet.ScimUser, totalResults uint, err error) {
|
||||
// Default pagination values if not provided
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
if opts.StartIndex == 0 {
|
||||
opts.StartIndex = 1
|
||||
}
|
||||
if opts.PerPage == 0 {
|
||||
opts.PerPage = SCIMDefaultResourcesPerPage
|
||||
}
|
||||
|
||||
// Calculate offset for pagination
|
||||
offset := (opts.Page - 1) * opts.PerPage
|
||||
|
||||
// Build the base query
|
||||
baseQuery := `
|
||||
SELECT DISTINCT
|
||||
@@ -298,7 +288,7 @@ func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersList
|
||||
|
||||
// Add pagination to the main query
|
||||
query := baseQuery + whereClause + " ORDER BY scim_users.id LIMIT ? OFFSET ?"
|
||||
params = append(params, opts.PerPage, offset)
|
||||
params = append(params, opts.PerPage, opts.StartIndex-1)
|
||||
|
||||
// Execute the query
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &users, query, params...)
|
||||
@@ -719,16 +709,13 @@ func (ds *Datastore) DeleteScimGroup(ctx context.Context, id uint) error {
|
||||
// ListScimGroups retrieves a list of SCIM groups with pagination
|
||||
func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOptions) (groups []fleet.ScimGroup, totalResults uint, err error) {
|
||||
// Default pagination values if not provided
|
||||
if opts.Page == 0 {
|
||||
opts.Page = 1
|
||||
if opts.StartIndex == 0 {
|
||||
opts.StartIndex = 1
|
||||
}
|
||||
if opts.PerPage == 0 {
|
||||
opts.PerPage = SCIMDefaultResourcesPerPage
|
||||
}
|
||||
|
||||
// Calculate offset for pagination
|
||||
offset := (opts.Page - 1) * opts.PerPage
|
||||
|
||||
// Build the query
|
||||
baseQuery := `
|
||||
SELECT DISTINCT
|
||||
@@ -745,7 +732,7 @@ func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOpti
|
||||
|
||||
// Add pagination to the main query
|
||||
query := baseQuery + " ORDER BY scim_groups.id LIMIT ? OFFSET ?"
|
||||
params := []interface{}{opts.PerPage, offset}
|
||||
params := []interface{}{opts.PerPage, opts.StartIndex - 1}
|
||||
|
||||
// Execute the query
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &groups, query, params...)
|
||||
|
||||
@@ -521,8 +521,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 1: List all users without filters
|
||||
allUsers, totalResults, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
@@ -553,8 +553,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 2: Pagination - first page with 2 items
|
||||
page1Users, totalPage1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 2,
|
||||
StartIndex: 1,
|
||||
PerPage: 2,
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
@@ -564,8 +564,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 3: Pagination - second page with 2 items
|
||||
page2Users, totalPage2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 2,
|
||||
PerPage: 2,
|
||||
StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3
|
||||
PerPage: 2,
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
@@ -582,8 +582,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 4: Filter by username
|
||||
listUsers, totalListUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
UserNameFilter: ptr.String("list-test-user2"),
|
||||
})
|
||||
@@ -596,8 +596,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 5: Filter by email type and value
|
||||
homeEmailUsers, totalHomeEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
EmailTypeFilter: ptr.String("home"),
|
||||
EmailValueFilter: ptr.String("personal.user2@example.com"),
|
||||
@@ -611,8 +611,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 6: Filter by email type and value - work emails
|
||||
workEmailUsers, totalWorkEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
EmailTypeFilter: ptr.String("work"),
|
||||
EmailValueFilter: ptr.String("different.user3@example.com"),
|
||||
@@ -624,8 +624,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
// Test 7: No results for non-matching filters
|
||||
noUsers, totalNoUsers1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
UserNameFilter: ptr.String("nonexistent"),
|
||||
})
|
||||
@@ -635,8 +635,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) {
|
||||
|
||||
noUsers, totalNoUsers2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{
|
||||
ScimListOptions: fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
},
|
||||
EmailTypeFilter: ptr.String("nonexistent"),
|
||||
EmailValueFilter: ptr.String("nonexistent"),
|
||||
@@ -1030,8 +1030,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
|
||||
|
||||
// Test 1: List all groups
|
||||
allGroups, totalResults, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 10,
|
||||
StartIndex: 1,
|
||||
PerPage: 10,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.GreaterOrEqual(t, len(allGroups), 3) // There might be other groups from previous tests
|
||||
@@ -1051,8 +1051,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
|
||||
|
||||
// Test 2: Pagination - first page with 2 items
|
||||
page1Groups, totalPage1, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
|
||||
Page: 1,
|
||||
PerPage: 2,
|
||||
StartIndex: 1,
|
||||
PerPage: 2,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 2, len(page1Groups))
|
||||
@@ -1060,8 +1060,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) {
|
||||
|
||||
// Test 3: Pagination - second page with 2 items
|
||||
page2Groups, totalPage2, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{
|
||||
Page: 2,
|
||||
PerPage: 2,
|
||||
StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3
|
||||
PerPage: 2,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.GreaterOrEqual(t, len(page2Groups), 1) // At least 1 item on the second page
|
||||
|
||||
@@ -25,8 +25,8 @@ type ScimUserEmail struct {
|
||||
}
|
||||
|
||||
type ScimListOptions struct {
|
||||
// Which page to return (must be positive integer)
|
||||
Page uint
|
||||
// 1-based index of the first result to return (must be positive integer)
|
||||
StartIndex uint
|
||||
// How many results per page (must be positive integer)
|
||||
PerPage uint
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ import (
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/android"
|
||||
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAndroid(t *testing.T) {
|
||||
s := integrationtest.SetUpSuite(t, "integrationtest.Android")
|
||||
s := SetUpSuite(t, "integrationtest.Android")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
fn func(t *testing.T, s *integrationtest.Suite)
|
||||
fn func(t *testing.T, s *Suite)
|
||||
}{
|
||||
{"HappyPath", testHappyPath},
|
||||
}
|
||||
@@ -27,14 +26,14 @@ func TestAndroid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testHappyPath(t *testing.T, s *integrationtest.Suite) {
|
||||
func testHappyPath(t *testing.T, s *Suite) {
|
||||
signupDetails := expectSignupDetails(t, s)
|
||||
var signupURL android.EnterpriseSignupResponse
|
||||
s.DoJSON(t, "GET", "/api/v1/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupURL)
|
||||
assert.Equal(t, signupURL.Url, signupDetails.Url)
|
||||
}
|
||||
|
||||
func expectSignupDetails(t *testing.T, s *integrationtest.Suite) *android.SignupDetails {
|
||||
func expectSignupDetails(t *testing.T, s *Suite) *android.SignupDetails {
|
||||
signupDetails := &android.SignupDetails{
|
||||
Url: "URL",
|
||||
Name: "Name",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package android
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
|
||||
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service/integrationtest"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Suite struct {
|
||||
integrationtest.BaseSuite
|
||||
AndroidProxy *android_mock.Proxy
|
||||
}
|
||||
|
||||
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
||||
ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName)
|
||||
logger := log.NewLogfmtLogger(os.Stdout)
|
||||
proxy := android_mock.Proxy{}
|
||||
proxy.InitCommonMocks()
|
||||
androidSvc, err := android_service.NewServiceWithProxy(
|
||||
logger,
|
||||
ds,
|
||||
&proxy,
|
||||
fleetSvc,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierFree,
|
||||
},
|
||||
FleetConfig: &fleetCfg,
|
||||
Pool: redisPool,
|
||||
Logger: logger,
|
||||
FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
|
||||
})
|
||||
|
||||
s := &Suite{
|
||||
BaseSuite: integrationtest.BaseSuite{
|
||||
Logger: logger,
|
||||
DS: ds,
|
||||
FleetCfg: fleetCfg,
|
||||
Users: users,
|
||||
Server: server,
|
||||
},
|
||||
AndroidProxy: &proxy,
|
||||
}
|
||||
|
||||
integrationtest.SetUpServerURL(t, ds, server)
|
||||
|
||||
s.Token = s.GetTestAdminToken(t)
|
||||
return s
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
|
||||
func (s *BaseSuite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
|
||||
resp := s.Do(t, verb, path, params, expectedStatusCode, queryParams...)
|
||||
err := json.UnmarshalRead(resp.Body, v)
|
||||
require.NoError(t, err)
|
||||
@@ -21,7 +21,7 @@ func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expe
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response {
|
||||
func (s *BaseSuite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response {
|
||||
j, err := json.Marshal(params)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -33,13 +33,13 @@ func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expected
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Suite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response {
|
||||
func (s *BaseSuite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response {
|
||||
return s.DoRawWithHeaders(t, verb, path, rawBytes, expectedStatusCode, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", s.Token),
|
||||
}, queryParams...)
|
||||
}
|
||||
|
||||
func (s *Suite) DoRawWithHeaders(
|
||||
func (s *BaseSuite) DoRawWithHeaders(
|
||||
t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, headers map[string]string, queryParams ...string,
|
||||
) *http.Response {
|
||||
return httptest.DoHTTPReq(t, decodeJSON, verb, rawBytes, s.Server.URL+path, headers, expectedStatusCode, queryParams...)
|
||||
|
||||
@@ -3,74 +3,53 @@ package integrationtest
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
|
||||
android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type Suite struct {
|
||||
Logger log.Logger
|
||||
FleetCfg config.FleetConfig
|
||||
Server *httptest.Server
|
||||
DS *mysql.Datastore
|
||||
Users map[string]fleet.User
|
||||
Token string
|
||||
AndroidProxy *android_mock.Proxy
|
||||
type BaseSuite struct {
|
||||
Logger log.Logger
|
||||
FleetCfg config.FleetConfig
|
||||
Server *httptest.Server
|
||||
DS *mysql.Datastore
|
||||
Users map[string]fleet.User
|
||||
Token string
|
||||
|
||||
cachedAdminToken string
|
||||
}
|
||||
|
||||
var testUsers = map[string]struct {
|
||||
Email string
|
||||
PlaintextPassword string
|
||||
GlobalRole *string
|
||||
}{
|
||||
"admin1": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "admin1@example.com",
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
},
|
||||
"user1": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "user1@example.com",
|
||||
GlobalRole: ptr.String(fleet.RoleMaintainer),
|
||||
},
|
||||
"user2": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "user2@example.com",
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
},
|
||||
}
|
||||
|
||||
func (s *Suite) GetTestAdminToken(t *testing.T) string {
|
||||
testUser := testUsers["admin1"]
|
||||
|
||||
func (s *BaseSuite) GetTestAdminToken(t *testing.T) string {
|
||||
// because the login endpoint is rate-limited, use the cached admin token
|
||||
// if available (if for some reason a test needs to logout the admin user,
|
||||
// then set cachedAdminToken = "" so that a new token is retrieved).
|
||||
if s.cachedAdminToken == "" {
|
||||
s.cachedAdminToken = s.GetTestToken(t, testUser.Email, testUser.PlaintextPassword)
|
||||
s.cachedAdminToken = s.GetTestToken(t, service.TestAdminUserEmail, test.GoodPassword)
|
||||
}
|
||||
return s.cachedAdminToken
|
||||
}
|
||||
|
||||
func (s *Suite) GetTestToken(t *testing.T, email string, password string) string {
|
||||
func (s *BaseSuite) GetTestToken(t *testing.T, email string, password string) string {
|
||||
return service.GetToken(t, email, password, s.Server.URL)
|
||||
}
|
||||
|
||||
func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
||||
func SetUpServerURL(t *testing.T, ds *mysql.Datastore, server *httptest.Server) {
|
||||
appConf, err := ds.AppConfig(t.Context())
|
||||
require.NoError(t, err)
|
||||
appConf.ServerSettings.ServerURL = server.URL
|
||||
err = ds.SaveAppConfig(t.Context(), appConf)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func SetUpDSRedisService(t *testing.T, uniqueTestName string) (*mysql.Datastore, fleet.RedisPool, config.FleetConfig,
|
||||
fleet.Service, context.Context) {
|
||||
ds := mysql.CreateMySQLDS(t)
|
||||
test.AddAllHostsLabel(t, ds)
|
||||
|
||||
@@ -85,44 +64,8 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
|
||||
redisPool := redistest.SetupRedis(t, uniqueTestName, false, false, false)
|
||||
|
||||
fleetCfg := config.TestConfig()
|
||||
logger := log.NewLogfmtLogger(os.Stdout)
|
||||
fleetSvc, ctx := service.NewTestService(t, ds, fleetCfg)
|
||||
proxy := android_mock.Proxy{}
|
||||
proxy.InitCommonMocks()
|
||||
androidSvc, err := android_service.NewServiceWithProxy(
|
||||
logger,
|
||||
ds,
|
||||
&proxy,
|
||||
fleetSvc,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{
|
||||
Tier: fleet.TierFree,
|
||||
},
|
||||
FleetConfig: &fleetCfg,
|
||||
Pool: redisPool,
|
||||
Logger: logger,
|
||||
FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)},
|
||||
})
|
||||
|
||||
s := &Suite{
|
||||
Logger: logger,
|
||||
DS: ds,
|
||||
FleetCfg: fleetCfg,
|
||||
Users: users,
|
||||
Server: server,
|
||||
AndroidProxy: &proxy,
|
||||
}
|
||||
|
||||
appConf, err = ds.AppConfig(ctx)
|
||||
require.NoError(t, err)
|
||||
appConf.ServerSettings.ServerURL = server.URL
|
||||
err = ds.SaveAppConfig(ctx, appConf)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.Token = s.GetTestAdminToken(t)
|
||||
return s
|
||||
return ds, redisPool, fleetCfg, fleetSvc, ctx
|
||||
}
|
||||
|
||||
func testContext() context.Context {
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/scim"
|
||||
eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
|
||||
"github.com/fleetdm/fleet/v4/ee/server/service/digicert"
|
||||
"github.com/fleetdm/fleet/v4/server/config"
|
||||
@@ -255,15 +255,11 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
|
||||
userID := uint(1)
|
||||
for _, key := range keys {
|
||||
u := testUsers[key]
|
||||
role := fleet.RoleObserver
|
||||
if strings.Contains(u.Email, "admin") {
|
||||
role = fleet.RoleAdmin
|
||||
}
|
||||
user := &fleet.User{
|
||||
ID: userID, // We need to set this in case ds is a mocked Datastore.
|
||||
Name: "Test Name " + u.Email,
|
||||
Email: u.Email,
|
||||
GlobalRole: &role,
|
||||
GlobalRole: u.GlobalRole,
|
||||
}
|
||||
err := user.SetPassword(u.PlaintextPassword, 10, 10)
|
||||
require.Nil(t, err)
|
||||
@@ -275,6 +271,12 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User {
|
||||
return users
|
||||
}
|
||||
|
||||
const (
|
||||
TestAdminUserEmail = "admin1@example.com"
|
||||
TestMaintainerUserEmail = "user1@example.com"
|
||||
TestObserverUserEmail = "user2@example.com"
|
||||
)
|
||||
|
||||
var testUsers = map[string]struct {
|
||||
Email string
|
||||
PlaintextPassword string
|
||||
@@ -282,17 +284,17 @@ var testUsers = map[string]struct {
|
||||
}{
|
||||
"admin1": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "admin1@example.com",
|
||||
Email: TestAdminUserEmail,
|
||||
GlobalRole: ptr.String(fleet.RoleAdmin),
|
||||
},
|
||||
"user1": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "user1@example.com",
|
||||
Email: TestMaintainerUserEmail,
|
||||
GlobalRole: ptr.String(fleet.RoleMaintainer),
|
||||
},
|
||||
"user2": {
|
||||
PlaintextPassword: test.GoodPassword,
|
||||
Email: "user2@example.com",
|
||||
Email: TestObserverUserEmail,
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
},
|
||||
}
|
||||
@@ -353,6 +355,7 @@ type TestServerOpts struct {
|
||||
FeatureRoutes []endpoint_utils.HandlerRoutesFunc
|
||||
SCEPConfigService fleet.SCEPConfigService
|
||||
DigiCertService fleet.DigiCertService
|
||||
EnableSCIM bool
|
||||
}
|
||||
|
||||
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
|
||||
@@ -451,6 +454,10 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl
|
||||
debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds)
|
||||
rootMux.Handle("/debug/", debugHandler)
|
||||
|
||||
if len(opts) > 0 && opts[0].EnableSCIM {
|
||||
require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger))
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(rootMux)
|
||||
server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux)
|
||||
// WriteTimeout is set for security purposes.
|
||||
|
||||
Reference in New Issue
Block a user