diff --git a/cmd/api/src/api/v2/apiclient/auth.go b/cmd/api/src/api/v2/apiclient/auth.go index d303ad1742..5849e86be8 100644 --- a/cmd/api/src/api/v2/apiclient/auth.go +++ b/cmd/api/src/api/v2/apiclient/auth.go @@ -17,39 +17,18 @@ package apiclient import ( - "bytes" "fmt" - "io" - "mime/multipart" "net/http" "net/url" - "strconv" "github.com/gofrs/uuid" - "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/src/api" v2 "github.com/specterops/bloodhound/src/api/v2" - authapi "github.com/specterops/bloodhound/src/api/v2/auth" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/model" ) -func (s Client) ListSAMLSignOnEndpoints() (v2.ListSAMLSignOnEndpointsResponse, error) { - var providersResponse v2.ListSAMLSignOnEndpointsResponse - - if response, err := s.Request(http.MethodGet, "api/v2/saml/sso", nil, nil); err != nil { - return providersResponse, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return providersResponse, ReadAPIError(response) - } - - return providersResponse, api.ReadAPIV2ResponsePayload(&providersResponse, response) - } -} - +// TODO when formally deprecated update this to another endpoint func (s Client) ListSAMLIdentityProviders() (v2.ListSAMLProvidersResponse, error) { var providersResponse v2.ListSAMLProvidersResponse @@ -66,57 +45,6 @@ func (s Client) ListSAMLIdentityProviders() (v2.ListSAMLProvidersResponse, error } } -func (s Client) CreateSAMLIdentityProvider(request v2.CreateSAMLAuthProviderRequest) (model.SAMLProvider, error) { - var newProvider model.SAMLProvider - - if response, err := s.Request(http.MethodPost, "api/v2/saml", nil, request); err != nil { - return newProvider, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return newProvider, ReadAPIError(response) - } - - return newProvider, api.ReadAPIV2ResponsePayload(&newProvider, response) - } -} - -func (s Client) CreateSAMLIdentityProviderMultipart(name, metadata string) (model.SAMLProvider, error) { - var ( - newProvider model.SAMLProvider - - buffer = &bytes.Buffer{} - header = make(http.Header) - multipartWriter = multipart.NewWriter(buffer) - ) - - if err := multipartWriter.WriteField("name", name); err != nil { - return newProvider, err - } else if fileWriter, err := multipartWriter.CreateFormFile("metadata", "metadata.xml"); err != nil { - return newProvider, err - } else { - if _, err := io.Copy(fileWriter, bytes.NewBufferString(metadata)); err != nil { - return newProvider, fmt.Errorf("failed to copy metadata to file: %w", err) - } - multipartWriter.Close() - - header.Set(headers.ContentType.String(), multipartWriter.FormDataContentType()) - - if response, err := s.Request(http.MethodPost, "api/v2/saml/providers", nil, buffer.Bytes(), header); err != nil { - return newProvider, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return newProvider, ReadAPIError(response) - } - - return newProvider, api.ReadJsonResponsePayload(&newProvider, response) - } - } -} - func (s Client) ListAuthTokens() (v2.ListTokensResponse, error) { var tokens v2.ListTokensResponse @@ -151,118 +79,6 @@ func (s Client) ListUserTokens(id uuid.UUID) (v2.ListTokensResponse, error) { } } -func (s Client) EnrollMFA(id uuid.UUID, secret string) (authapi.MFAEnrollmentReponse, error) { - var ( - enrollmentResponse authapi.MFAEnrollmentReponse - payload = authapi.MFAEnrollmentRequest{ - Secret: secret, - } - ) - - if response, err := s.Request(http.MethodPost, fmt.Sprintf("api/v2/bloodhound-users/%s/mfa", id), nil, payload); err != nil { - return enrollmentResponse, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return enrollmentResponse, ReadAPIError(response) - } - - return enrollmentResponse, api.ReadAPIV2ResponsePayload(&enrollmentResponse, response) - } -} - -func (s Client) ActivateMFA(id uuid.UUID, otp string) (authapi.MFAStatusResponse, error) { - var ( - mfaStatusResponse authapi.MFAStatusResponse - payload = authapi.MFAActivationRequest{ - OTP: otp, - } - ) - - if response, err := s.Request(http.MethodPost, fmt.Sprintf("api/v2/bloodhound-users/%s/mfa-activation", id), nil, payload); err != nil { - return mfaStatusResponse, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return mfaStatusResponse, ReadAPIError(response) - } - - return mfaStatusResponse, api.ReadAPIV2ResponsePayload(&mfaStatusResponse, response) - } -} - -func (s Client) GetMFAActivationStatus(id uuid.UUID) (authapi.MFAStatusResponse, error) { - var mfaStatusResponse authapi.MFAStatusResponse - - if response, err := s.Request(http.MethodGet, fmt.Sprintf("api/v2/bloodhound-users/%s/mfa-activation", id), nil, nil); err != nil { - return mfaStatusResponse, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return mfaStatusResponse, ReadAPIError(response) - } - - return mfaStatusResponse, api.ReadAPIV2ResponsePayload(&mfaStatusResponse, response) - } -} - -func (s Client) LookupSelf() (model.User, error) { - var self model.User - if response, err := s.Request(http.MethodGet, "api/v2/auth/self", nil, nil); err != nil { - return self, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return self, ReadAPIError(response) - } - - return self, api.ReadAPIV2ResponsePayload(&self, response) - } -} - -func (s Client) CreateSAMLUser(userPrincipal, userEmailAddress string, samlProviderID int32, roles []int32) (model.User, error) { - var newUser model.User - - payload := v2.CreateUserRequest{ - UpdateUserRequest: v2.UpdateUserRequest{ - Principal: userPrincipal, - EmailAddress: userEmailAddress, - Roles: roles, - SAMLProviderID: strconv.FormatInt(int64(samlProviderID), 10), - }, - } - - if response, err := s.Request(http.MethodPost, "api/v2/bloodhound-users", nil, payload); err != nil { - return newUser, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return newUser, ReadAPIError(response) - } - - return newUser, api.ReadAPIV2ResponsePayload(&newUser, response) - } -} - -func (s Client) UpdateUser(userID uuid.UUID, updateUserRequest v2.UpdateUserRequest) error { - if response, err := s.Request(http.MethodPut, fmt.Sprintf("api/v2/bloodhound-users/%s", userID), nil, updateUserRequest); err != nil { - return err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return ReadAPIError(response) - } - - return nil - } -} - func (s Client) CreateUser(userPrincipal, userEmailAddress string, roles []int32) (model.User, error) { var newUser model.User @@ -301,22 +117,6 @@ func (s Client) DeleteUser(userID uuid.UUID) error { return nil } -func (s Client) GetUser(id uuid.UUID) (model.User, error) { - var user model.User - - if response, err := s.Request(http.MethodGet, fmt.Sprintf("api/v2/bloodhound-users/%s", id), nil, nil); err != nil { - return user, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return user, ReadAPIError(response) - } - - return user, api.ReadAPIV2ResponsePayload(&user, response) - } -} - func (s Client) ListUsers() (v2.ListUsersResponse, error) { var users v2.ListUsersResponse @@ -333,50 +133,6 @@ func (s Client) ListUsers() (v2.ListUsersResponse, error) { } } -func (s Client) UserAddRole(userID uuid.UUID, roleID int32) error { - if response, err := s.Request(http.MethodPost, fmt.Sprintf("api/v2/bloodhound-users/%s/roles/%d", userID, roleID), nil, nil); err != nil { - return err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return ReadAPIError(response) - } - } - - return nil -} - -func (s Client) UserRemoveRole(userID uuid.UUID, roleID int32) error { - if response, err := s.Request(http.MethodDelete, fmt.Sprintf("api/v2/bloodhound-users/%s/roles/%d", userID, roleID), nil, nil); err != nil { - return err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return ReadAPIError(response) - } - } - - return nil -} - -func (s Client) GetPermission(id int32) (model.Permission, error) { - var permission model.Permission - - if response, err := s.Request(http.MethodGet, fmt.Sprintf("api/v2/permissions/%d", id), nil, nil); err != nil { - return permission, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return permission, ReadAPIError(response) - } - - return permission, api.ReadAPIV2ResponsePayload(&permission, response) - } -} - func (s Client) ListPermissions() (v2.ListPermissionsResponse, error) { var permissions v2.ListPermissionsResponse @@ -393,22 +149,6 @@ func (s Client) ListPermissions() (v2.ListPermissionsResponse, error) { } } -func (s Client) GetRole(id int32) (model.Role, error) { - var role model.Role - - if response, err := s.Request(http.MethodGet, fmt.Sprintf("api/v2/auth/roles/%d", id), nil, nil); err != nil { - return role, err - } else { - defer response.Body.Close() - - if api.IsErrorResponse(response) { - return role, ReadAPIError(response) - } - - return role, api.ReadAPIV2ResponsePayload(&role, response) - } -} - func (s Client) ListRoles() (v2.ListRolesResponse, error) { var roles v2.ListRolesResponse @@ -516,7 +256,3 @@ func (s Client) LoginSecret(username, secret string) (api.LoginResponse, error) return loginResponse, api.ReadAPIV2ResponsePayload(&loginResponse, response) } } - -func (s Client) LoginSAML(organization, username string) error { - panic("TODO") -} diff --git a/cmd/api/src/api/v2/auth/auth_test.go b/cmd/api/src/api/v2/auth/auth_test.go index 0226144351..83a2bdfd95 100644 --- a/cmd/api/src/api/v2/auth/auth_test.go +++ b/cmd/api/src/api/v2/auth/auth_test.go @@ -233,12 +233,14 @@ func TestManagementResource_EnableUserSAML(t *testing.T) { func TestManagementResource_DeleteSAMLProvider(t *testing.T) { var ( goodSAMLProvider = model.SAMLProvider{ + SSOProviderID: null.Int32From(1), Serial: model.Serial{ ID: 1, }, } samlProviderWithUsers = model.SAMLProvider{ + SSOProviderID: null.Int32From(2), Serial: model.Serial{ ID: 2, }, @@ -256,34 +258,35 @@ func TestManagementResource_DeleteSAMLProvider(t *testing.T) { defer mockCtrl.Finish() - mockDB.EXPECT().GetSAMLProvider(gomock.Any(), goodSAMLProvider.ID).Return(goodSAMLProvider, nil) - mockDB.EXPECT().GetSAMLProvider(gomock.Any(), samlProviderWithUsers.ID).Return(samlProviderWithUsers, nil) - mockDB.EXPECT().DeleteSSOProvider(gomock.Any(), gomock.Eq(int(goodSAMLProvider.SSOProviderID.Int32))).Return(nil) - mockDB.EXPECT().DeleteSSOProvider(gomock.Any(), gomock.Eq(int(samlProviderWithUsers.SSOProviderID.Int32))).Return(nil) - mockDB.EXPECT().GetSAMLProviderUsers(gomock.Any(), goodSAMLProvider.ID).Return(nil, nil) - mockDB.EXPECT().GetSAMLProviderUsers(gomock.Any(), samlProviderWithUsers.ID).Return(model.Users{samlEnabledUser}, nil) - - // Happy path - test.Request(t). - WithMethod(http.MethodDelete). - WithURL(fmt.Sprintf(samlProviderPathFmt, goodSAMLProvider.ID)). //nolint:govet // Ignore non-constant format string failure because it's test code - WithURLPathVars(map[string]string{ - api.URIPathVariableSAMLProviderID: fmt.Sprintf("%d", goodSAMLProvider.ID), - }). - OnHandlerFunc(resources.DeleteSAMLProvider). - Require(). - ResponseStatusCode(http.StatusOK) + t.Run("successfully deletes saml provider", func(t *testing.T) { + mockDB.EXPECT().GetSAMLProvider(gomock.Any(), goodSAMLProvider.ID).Return(goodSAMLProvider, nil) + mockDB.EXPECT().DeleteSSOProvider(gomock.Any(), gomock.Eq(int(goodSAMLProvider.SSOProviderID.Int32))).Return(nil) + mockDB.EXPECT().GetSSOProviderUsers(gomock.Any(), int(goodSAMLProvider.ID)).Return(nil, nil) + test.Request(t). + WithMethod(http.MethodDelete). + WithURL(fmt.Sprintf(samlProviderPathFmt, goodSAMLProvider.ID)). //nolint:govet // Ignore non-constant format string failure because it's test code + WithURLPathVars(map[string]string{ + api.URIPathVariableSAMLProviderID: fmt.Sprintf("%d", goodSAMLProvider.ID), + }). + OnHandlerFunc(resources.DeleteSAMLProvider). + Require(). + ResponseStatusCode(http.StatusOK) + }) - // Negative path where a provider has attached users - test.Request(t). - WithMethod(http.MethodDelete). - WithURL(fmt.Sprintf(samlProviderPathFmt, samlProviderWithUsers.ID)). //nolint:govet // Ignore non-constant format string failure because it's test code - WithURLPathVars(map[string]string{ - api.URIPathVariableSAMLProviderID: fmt.Sprintf("%d", samlProviderWithUsers.ID), - }). - OnHandlerFunc(resources.DeleteSAMLProvider). - Require(). - ResponseStatusCode(http.StatusOK) + t.Run("fails when provider has attached users", func(t *testing.T) { + mockDB.EXPECT().GetSAMLProvider(gomock.Any(), samlProviderWithUsers.ID).Return(samlProviderWithUsers, nil) + mockDB.EXPECT().DeleteSSOProvider(gomock.Any(), gomock.Eq(int(samlProviderWithUsers.SSOProviderID.Int32))).Return(nil) + mockDB.EXPECT().GetSSOProviderUsers(gomock.Any(), int(samlProviderWithUsers.ID)).Return(model.Users{samlEnabledUser}, nil) + test.Request(t). + WithMethod(http.MethodDelete). + WithURL(fmt.Sprintf(samlProviderPathFmt, samlProviderWithUsers.ID)). //nolint:govet // Ignore non-constant format string failure because it's test code + WithURLPathVars(map[string]string{ + api.URIPathVariableSAMLProviderID: fmt.Sprintf("%d", samlProviderWithUsers.ID), + }). + OnHandlerFunc(resources.DeleteSAMLProvider). + Require(). + ResponseStatusCode(http.StatusOK) + }) } func TestManagementResource_ListPermissions_SortingError(t *testing.T) { diff --git a/cmd/api/src/api/v2/model.go b/cmd/api/src/api/v2/model.go index 22cfab02bb..c751ba22d1 100644 --- a/cmd/api/src/api/v2/model.go +++ b/cmd/api/src/api/v2/model.go @@ -91,47 +91,10 @@ type CreateUserToken struct { UserID string `json:"user_id"` } -type CreateSAMLAuthProviderRequest struct { - Name string `json:"name"` - DisplayName string `json:"display_name"` - SigningCertificate string `json:"signing_certificate"` - IssuerURI string `json:"issuer_uri"` - SingleSignOnURI string `json:"single_signon_uri"` - PrincipalAttributeMappings []string `json:"principal_attribute_mappings"` -} - -type UpdateSAMLAuthProviderRequest struct { - Name string `json:"name"` - DisplayName string `json:"display_name"` - SigningCertificate string `json:"signing_certificate"` - IssuerURI string `json:"issuer_uri"` - SingleSignOnURI string `json:"single_signon_uri"` - PrincipalAttributeMappings []string `json:"principal_attribute_mappings"` -} - -type SecretInitializationRequest struct { - AdminEmailAddress string `json:"admin_email_address"` - Secret string `json:"secret"` -} - -type IDPValidationResponse struct { - ErrorMessage string `json:"error_message"` - Successful bool `json:"successful"` -} - -type PagedNodeListEntry struct { - Name string `json:"name"` - Type string `json:"type"` - DistinguishedName string `json:"distinguished_name"` - ObjectID string `json:"object_id"` -} - -type SAMLInitializationRequest struct { - AdminEmailAddress string `json:"admin_email_address"` - IdentityProviderProviderName string `json:"idp_name"` - IdentityProviderURL string `json:"idp_url"` - ServiceProviderCertificate string `json:"sp_certificate"` - ServiceProviderKey string `json:"sp_private_key"` +type CreateOIDCProviderRequest struct { + Name string `json:"name"` + Issuer string `json:"issuer"` + ClientId string `json:"client_id"` } // Resources holds the database and configuration dependencies to be passed around the API functions diff --git a/cmd/api/src/auth/role_test.go b/cmd/api/src/auth/role_test.go index c5b48b8ff2..d59a517f93 100644 --- a/cmd/api/src/auth/role_test.go +++ b/cmd/api/src/auth/role_test.go @@ -141,6 +141,7 @@ func testRoleAccess(t *testing.T, roleName string) { userClient, ok := lab.Unpack(harness, userClientFixture) assert.True(ok) + // TODO when formally deprecated update this to another endpoint _, err := userClient.ListSAMLIdentityProviders() if role.Permissions.Has(auth.Permissions().AuthManageProviders) { assert.Nil(err) diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index e10150c0c3..271430e0dc 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -1441,21 +1441,6 @@ func (mr *MockDatabaseMockRecorder) LookupActiveSessionsByUser(arg0, arg1 interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupActiveSessionsByUser", reflect.TypeOf((*MockDatabase)(nil).LookupActiveSessionsByUser), arg0, arg1) } -// LookupSAMLProviderByName mocks base method. -func (m *MockDatabase) LookupSAMLProviderByName(arg0 context.Context, arg1 string) (model.SAMLProvider, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LookupSAMLProviderByName", arg0, arg1) - ret0, _ := ret[0].(model.SAMLProvider) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// LookupSAMLProviderByName indicates an expected call of LookupSAMLProviderByName. -func (mr *MockDatabaseMockRecorder) LookupSAMLProviderByName(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupSAMLProviderByName", reflect.TypeOf((*MockDatabase)(nil).LookupSAMLProviderByName), arg0, arg1) -} - // LookupUser mocks base method. func (m *MockDatabase) LookupUser(arg0 context.Context, arg1 string) (model.User, error) { m.ctrl.T.Helper() diff --git a/cmd/api/src/go.mod b/cmd/api/src/go.mod index 614632c30c..604d304ee1 100644 --- a/cmd/api/src/go.mod +++ b/cmd/api/src/go.mod @@ -19,7 +19,6 @@ module github.com/specterops/bloodhound/src go 1.23 require ( - github.com/beevik/etree v1.2.0 github.com/bloodhoundad/azurehound/v2 v2.0.1 github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 github.com/coreos/go-oidc/v3 v3.11.0 @@ -35,7 +34,6 @@ require ( github.com/gorilla/schema v1.4.1 github.com/jackc/pgx/v5 v5.7.1 github.com/jedib0t/go-pretty/v6 v6.4.6 - github.com/mattermost/xml-roundtrip-validator v0.1.0 github.com/neo4j/neo4j-go-driver/v5 v5.9.0 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 @@ -44,9 +42,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/teambition/rrule-go v1.8.2 github.com/unrolled/secure v1.13.0 - github.com/zenazn/goji v1.0.1 go.uber.org/mock v0.2.0 - golang.org/x/crypto v0.27.0 golang.org/x/oauth2 v0.23.0 gorm.io/driver/postgres v1.3.8 gorm.io/gorm v1.23.8 @@ -54,6 +50,7 @@ require ( require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/beevik/etree v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -76,6 +73,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/lib/pq v1.10.9 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -83,6 +81,7 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect + golang.org/x/crypto v0.27.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/cmd/api/src/go.sum b/cmd/api/src/go.sum index c1b289e1c1..decdd7d534 100644 --- a/cmd/api/src/go.sum +++ b/cmd/api/src/go.sum @@ -220,8 +220,6 @@ github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFs github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= -github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/cmd/api/src/test/integration/server.go b/cmd/api/src/test/integration/server.go deleted file mode 100644 index 12ea05df3b..0000000000 --- a/cmd/api/src/test/integration/server.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2023 Specter Ops, Inc. -// -// Licensed under the Apache License, Version 2.0 -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// SPDX-License-Identifier: Apache-2.0 - -package integration