diff --git a/cmd/api/src/auth/permission.go b/cmd/api/src/auth/permission.go index 7ef503549d..0f54486d11 100644 --- a/cmd/api/src/auth/permission.go +++ b/cmd/api/src/auth/permission.go @@ -74,6 +74,7 @@ func (s PermissionSet) All() model.Permissions { } } +// Permissions Note: Not the only source of truth, changes here must be added to a migration *.sql file to update the permissions table func Permissions() PermissionSet { return PermissionSet{ AppReadApplicationConfiguration: model.NewPermission("app", "ReadAppConfig"), diff --git a/cmd/api/src/auth/role.go b/cmd/api/src/auth/role.go index 44418bbc6e..1c27e7cbd2 100644 --- a/cmd/api/src/auth/role.go +++ b/cmd/api/src/auth/role.go @@ -17,8 +17,6 @@ package auth import ( - "fmt" - "github.com/specterops/bloodhound/src/model" ) @@ -36,33 +34,7 @@ type RoleTemplate struct { Permissions model.Permissions } -func (s RoleTemplate) Build(allPermissions model.Permissions) (model.Role, error) { - role := model.Role{ - Name: s.Name, - Description: s.Description, - Permissions: make(model.Permissions, len(s.Permissions)), - } - - for idx, requiredPermission := range s.Permissions { - found := false - - for _, permission := range allPermissions { - if permission.Equals(requiredPermission) { - role.Permissions[idx] = permission - found = true - - break - } - } - - if !found { - return role, fmt.Errorf("unable to locate required permission %s for role template %s", requiredPermission, s.Name) - } - } - - return role, nil -} - +// Roles Note: Not the source of truth, changes here must be added to a migration *.sql file to update the roles & roles_permissions table func Roles() map[string]RoleTemplate { permissions := Permissions() diff --git a/cmd/api/src/database/db.go b/cmd/api/src/database/db.go index 1aa0bb5ad5..a5965fdd97 100644 --- a/cmd/api/src/database/db.go +++ b/cmd/api/src/database/db.go @@ -85,7 +85,6 @@ type Database interface { Wipe(ctx context.Context) error Migrate(ctx context.Context) error - RequiresMigration(ctx context.Context) (bool, error) CreateInstallation(ctx context.Context) (model.Installation, error) GetInstallation(ctx context.Context) (model.Installation, error) HasInstallation(ctx context.Context) (bool, error) @@ -241,13 +240,9 @@ func (s *BloodhoundDB) Wipe(ctx context.Context) error { }) } -func (s *BloodhoundDB) RequiresMigration(ctx context.Context) (bool, error) { - return migration.NewMigrator(s.db.WithContext(ctx)).RequiresMigration() -} - func (s *BloodhoundDB) Migrate(ctx context.Context) error { // Run the migrator - if err := migration.NewMigrator(s.db.WithContext(ctx)).Migrate(); err != nil { + if err := migration.NewMigrator(s.db.WithContext(ctx)).ExecuteStepwiseMigrations(); err != nil { log.Errorf("Error during SQL database migration phase: %v", err) return err } diff --git a/cmd/api/src/database/migration/agi.go b/cmd/api/src/database/migration/agi.go deleted file mode 100644 index 3ab67db1bb..0000000000 --- a/cmd/api/src/database/migration/agi.go +++ /dev/null @@ -1,102 +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 migration - -import ( - "regexp" - - "github.com/specterops/bloodhound/log" - "github.com/specterops/bloodhound/src/model" - "gorm.io/gorm" -) - -var ( - selectorRegexes = []*regexp.Regexp{ - regexp.MustCompile(`match\s+\(t\)\s+WHERE\s+\(.+\)\s+AND\s+t\.objectid="([^"]+)"`), - regexp.MustCompile(`match\s+\([^{]+\{objectid:\s+"([^"]+)"\}\)`), - } -) - -func SelectorToObjectID(rawSelector string) string { - for _, selectorRegex := range selectorRegexes { - if matches := selectorRegex.FindStringSubmatch(rawSelector); len(matches) == 2 { - return matches[1] - } - } - - return rawSelector -} - -func (s *Migrator) updateAssetGroups() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - var systemAssetGroups model.AssetGroups - - // Lookup system asset groups - if result := tx.Where("system_group = true").Find(&systemAssetGroups); result.Error != nil { - return result.Error - } - - // Create asset groups if they don't already exist - if _, hasTierZero := systemAssetGroups.FindByName(model.TierZeroAssetGroupName); !hasTierZero { - log.Infof("Missing the default Admin Tier Zero asset group. Creating it now.") - - newTierZeroAG := model.AssetGroup{ - Name: model.TierZeroAssetGroupName, - Tag: model.TierZeroAssetGroupTag, - SystemGroup: true, - } - - if result := tx.Create(&newTierZeroAG); result.Error != nil { - return result.Error - } - } - - if _, hasOwned := systemAssetGroups.FindByName(model.OwnedAssetGroupName); !hasOwned { - log.Infof("Missing the default Owned asset group. Creating it now.") - - ownedAG := model.AssetGroup{ - Name: model.OwnedAssetGroupName, - Tag: model.OwnedAssetGroupTag, - SystemGroup: true, - } - - if result := tx.Create(&ownedAG); result.Error != nil { - return result.Error - } - } - - // Load the AG selectors to migrate the selectors away from cypher - for _, assetGroup := range systemAssetGroups { - var selectors model.AssetGroupSelectors - - if result := tx.Where("asset_group_id = ?", assetGroup.ID).Find(&selectors); result.Error != nil { - return result.Error - } - - for _, selector := range selectors { - oldSelector := selector.Selector - - if rewrittenSelector := SelectorToObjectID(oldSelector); rewrittenSelector != oldSelector { - selector.Selector = rewrittenSelector - tx.Save(selector) - } - } - } - - return nil - }) -} diff --git a/cmd/api/src/database/migration/agi_test.go b/cmd/api/src/database/migration/agi_test.go deleted file mode 100644 index 95c657320f..0000000000 --- a/cmd/api/src/database/migration/agi_test.go +++ /dev/null @@ -1,29 +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 migration - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestSelectorToObjectID(t *testing.T) { - require.Equal(t, "nope", SelectorToObjectID(`nope`)) - require.Equal(t, "S-1-5-21-12345-12345-12345-12345", SelectorToObjectID(`match (t) WHERE (t:Base) AND t.objectid="S-1-5-21-12345-12345-12345-12345"`)) - require.Equal(t, "S-1-5-21-12345-12345-12345-12345", SelectorToObjectID(`match (t :Base {objectid: "S-1-5-21-12345-12345-12345-12345"})`)) -} diff --git a/cmd/api/src/database/migration/app_config.go b/cmd/api/src/database/migration/app_config.go deleted file mode 100644 index b8df175e9e..0000000000 --- a/cmd/api/src/database/migration/app_config.go +++ /dev/null @@ -1,77 +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 migration - -import ( - "fmt" - - "github.com/specterops/bloodhound/log" - "github.com/specterops/bloodhound/src/model/appcfg" - "gorm.io/gorm" -) - -func (s *Migrator) setAppConfigDefaults() error { - if err := s.setParameterDefaults(); err != nil { - return err - } - - return s.setFeatureFlagDefaults() -} - -func (s *Migrator) setFeatureFlagDefaults() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - for flagKey, availableFlag := range appcfg.AvailableFlags() { - count := int64(0) - - if result := tx.Model(&appcfg.FeatureFlag{}).Where("key = ?", flagKey).Count(&count); count == 0 { - if result := tx.Create(&availableFlag); result.Error != nil { - return fmt.Errorf("error creating feature flag %s: %w", flagKey, result.Error) - } - - log.Infof("Feature flag %s created", flagKey) - } else if result.Error != nil { - return fmt.Errorf("error looking up existing feature flag %s: %w", flagKey, result.Error) - } - } - - return nil - }) -} - -func (s *Migrator) setParameterDefaults() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - if availParams, err := appcfg.AvailableParameters(); err != nil { - return fmt.Errorf("error checking AvailableParameters: %w", err) - } else { - for parameterKey, availableParameter := range availParams { - count := int64(0) - - if result := tx.Model(&appcfg.Parameter{}).Where("key = ?", parameterKey).Count(&count); count == 0 { - if result := tx.Create(&availableParameter); result.Error != nil { - return fmt.Errorf("error setting configuration parameter %s(%s): %w", parameterKey, availableParameter.Name, result.Error) - } - - log.Infof("Configuration parameter %s created", parameterKey) - } else if result.Error != nil { - return fmt.Errorf("error looking up existing feature flag %s: %w", parameterKey, result.Error) - } - } - - return nil - } - }) -} diff --git a/cmd/api/src/database/migration/auth.go b/cmd/api/src/database/migration/auth.go deleted file mode 100644 index 42588057de..0000000000 --- a/cmd/api/src/database/migration/auth.go +++ /dev/null @@ -1,148 +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 migration - -import ( - "strings" - - "github.com/specterops/bloodhound/log" - "github.com/specterops/bloodhound/src/auth" - "github.com/specterops/bloodhound/src/model" - "gorm.io/gorm" -) - -func preload(tx *gorm.DB, associationSpecs []string) *gorm.DB { - cursor := tx - for _, associationSpec := range associationSpecs { - cursor = cursor.Preload(associationSpec) - } - - return cursor -} - -func getAllPermissions(tx *gorm.DB) (model.Permissions, error) { - var existingPermissions model.Permissions - return existingPermissions, tx.Find(&existingPermissions).Error -} - -func getAllRoles(tx *gorm.DB) (model.Roles, error) { - var roles model.Roles - return roles, preload(tx, model.RoleAssociations()).Find(&roles).Error -} - -func (s *Migrator) updatePermissions() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - if existingPermissions, err := getAllPermissions(tx); err != nil { - return err - } else { - for _, expectedPermission := range auth.Permissions().All() { - if !existingPermissions.Has(expectedPermission) { - if result := tx.Create(&expectedPermission); result.Error != nil { - return result.Error - } - - log.Infof("Permission %s created during migration", expectedPermission) - } - } - } - - return nil - }) -} - -func (s *Migrator) checkUserEmailAddresses() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - var users model.Users - - for _, userAssociation := range model.UserAssociations() { - tx.Preload(userAssociation) - } - - if result := tx.Find(&users); result.Error != nil { - return result.Error - } else { - seenAddresses := make(map[string]struct{}) - - for _, user := range users { - if !user.EmailAddress.Valid || len(user.EmailAddress.String) == 0 { - log.Errorf("UPNTE Error: user %s is missing a valid email address.", user.ID) - } else { - emailAddress := strings.ToLower(user.EmailAddress.String) - - if _, alreadySawAddress := seenAddresses[emailAddress]; alreadySawAddress { - log.Errorf("UPNTE Error: user %s contains a non-unique email address.", user.ID) - } - - seenAddresses[emailAddress] = struct{}{} - } - } - } - - return nil - }) -} - -func (s *Migrator) updateRoles() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - if permissions, err := getAllPermissions(tx); err != nil { - return err - } else if existingRoles, err := getAllRoles(tx); err != nil { - return err - } else { - for _, expectedRoleTemplate := range auth.Roles() { - - if expectedRole, err := expectedRoleTemplate.Build(permissions); err != nil { - return err - - } else if existingRole, found := existingRoles.FindByName(expectedRole.Name); !found { - // If cannot find by name, lookup by permissions set - if existingRole, ok := existingRoles.FindByPermissions(expectedRole.Permissions); !ok { - // If no Role exists w/ expectedPermissions, create new Role - if result := tx.Create(&expectedRole); result.Error != nil { - return result.Error - } - log.Infof("Role %s created during migration", expectedRole.Name) - - } else { - // A role with the required permission set exists but has changed, update the preexisting role - existingRole.Name = expectedRole.Name - existingRole.Description = expectedRole.Description - - if result := s.DB.Save(existingRole); result.Error != nil { - return result.Error - } - - log.Infof("Role %s updated during migration", expectedRole.Name) - } - - } else if !expectedRole.Permissions.Equals(existingRole.Permissions) || expectedRole.Description != existingRole.Description { - // The role for the associated name has changed, update the preexisting role - existingRole.Permissions = expectedRole.Permissions - existingRole.Description = expectedRole.Description - - if result := s.DB.Session(&gorm.Session{FullSaveAssociations: true}).Updates(existingRole); result.Error != nil { - return result.Error - } - - log.Infof("Role %s updated during migration", expectedRole.Name) - } - } - } - - return nil - }) -} diff --git a/cmd/api/src/database/migration/cleanup.go b/cmd/api/src/database/migration/cleanup.go deleted file mode 100644 index 9261500298..0000000000 --- a/cmd/api/src/database/migration/cleanup.go +++ /dev/null @@ -1,29 +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 migration - -import "gorm.io/gorm" - -func (s *Migrator) cleanupIngest() error { - return s.DB.Transaction(func(tx *gorm.DB) error { - if result := tx.Exec(`truncate table ingest_tasks;`); result.Error != nil { - return result.Error - } - - return nil - }) -} diff --git a/cmd/api/src/database/migration/migration.go b/cmd/api/src/database/migration/migration.go index 34404ad277..8bb760d5e8 100644 --- a/cmd/api/src/database/migration/migration.go +++ b/cmd/api/src/database/migration/migration.go @@ -18,7 +18,6 @@ package migration import ( "embed" - "fmt" "io/fs" "github.com/specterops/bloodhound/src/version" @@ -56,35 +55,3 @@ func NewMigrator(db *gorm.DB) *Migrator { DB: db, } } - -func (s *Migrator) Migrate() error { - if err := s.executeStepwiseMigrations(); err != nil { - return fmt.Errorf("failed to execute stepwise migrations: %w", err) - } - - if err := s.cleanupIngest(); err != nil { - return err - } - - if err := s.updatePermissions(); err != nil { - return err - } - - if err := s.updateRoles(); err != nil { - return err - } - - if err := s.updateAssetGroups(); err != nil { - return err - } - - if err := s.setAppConfigDefaults(); err != nil { - return err - } - - if err := s.checkUserEmailAddresses(); err != nil { - return err - } - - return nil -} diff --git a/cmd/api/src/database/migration/migration_integration_test.go b/cmd/api/src/database/migration/migration_integration_test.go index a6c264cb79..0061423730 100644 --- a/cmd/api/src/database/migration/migration_integration_test.go +++ b/cmd/api/src/database/migration/migration_integration_test.go @@ -286,7 +286,7 @@ func TestMigrator_Migrate(t *testing.T) { manifest, err := migrator.GenerateManifest() require.Nil(t, err) - assert.Nil(t, migrator.Migrate()) + assert.Nil(t, migrator.ExecuteStepwiseMigrations()) lastVersionInManifest := manifest.VersionTable[len(manifest.VersionTable)-1] latestMigration, err := migrator.LatestMigration() diff --git a/cmd/api/src/database/migration/migrations/schema.sql b/cmd/api/src/database/migration/migrations/schema.sql index 1ef4739017..595d1f11e6 100644 --- a/cmd/api/src/database/migration/migrations/schema.sql +++ b/cmd/api/src/database/migration/migrations/schema.sql @@ -587,3 +587,77 @@ ALTER TABLE ONLY users_roles ADD CONSTRAINT fk_users_roles_user FOREIGN KEY (user_id) REFERENCES users(id); ALTER TABLE ONLY users ADD CONSTRAINT fk_users_saml_provider FOREIGN KEY (saml_provider_id) REFERENCES saml_providers(id); + +-- Populate asset group table +INSERT INTO asset_groups (name, tag, system_group, created_at, updated_at) VALUES ('Admin Tier Zero', 'admin_tier_0', true, current_timestamp, current_timestamp); + +-- Populate permissions table +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('app', 'ReadAppConfig', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('app', 'WriteAppConfig', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('risks', 'GenerateReport', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('risks', 'ManageRisks', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('auth', 'CreateToken', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('auth', 'ManageAppConfig', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('auth', 'ManageProviders', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('auth', 'ManageSelf', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('auth', 'ManageUsers', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('clients', 'Manage', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('clients', 'Tasking', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('collection', 'ManageJobs', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('graphdb', 'Read', current_timestamp, current_timestamp); +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('graphdb', 'Write', current_timestamp, current_timestamp); + +-- Populate roles table +INSERT INTO roles (name, description, created_at, updated_at) VALUES ('Administrator', 'Can manage users, clients, and application configuration', current_timestamp, current_timestamp); +INSERT INTO roles (name, description, created_at, updated_at) VALUES ('User', 'Can read data, modify asset group memberships', current_timestamp, current_timestamp); +INSERT INTO roles (name, description, created_at, updated_at) VALUES ('Read-Only', 'Used for integrations', current_timestamp, current_timestamp); +INSERT INTO roles (name, description, created_at, updated_at) VALUES ('Upload-Only', 'Used for data collection clients, can post data but cannot read data', current_timestamp, current_timestamp); + + +-- Populate roles_permissions table +-- Administrator +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'ReadAppConfig')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'WriteAppConfig')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'GenerateReport')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'ManageRisks')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'CreateToken')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageAppConfig')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageProviders')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageSelf')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageUsers')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Manage')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Tasking')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'collection' and permissions.name = 'ManageJobs')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Read')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Write')); + +-- User +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'ReadAppConfig')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'GenerateReport')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'CreateToken')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageSelf')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Manage')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Read')); + +-- Read-Only +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'ReadAppConfig')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'GenerateReport')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageSelf')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Read')); + +-- Upload-Only +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Upload-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Tasking')); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Upload-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Write')); + +-- Populate feature_flags table +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'butterfly_analysis', 'Enhanced Asset Inbound-Outbound Exposure Analysis', 'Enables more extensive analysis of attack path findings that allows BloodHound to help the user prioritize remediation of the most exposed assets.', false, false); +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'enable_saml_sso', 'SAML Single Sign-On Support', 'Enables SSO authentication flows and administration panels to third party SAML identity providers.', true, false); +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'scope_collection_by_ou', 'Enable SharpHound OU Scoped Collections', 'Enables scoping SharpHound collections to specific lists of OUs.', true, false); +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'azure_support', 'Enable Azure Support', 'Enables Azure support.', true, false); +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'reconciliation', 'Reconciliation', 'Enables Reconciliation', true, false); +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'entity_panel_cache', 'Enable application level caching', 'Enables the use of application level caching for entity panel queries', true, false); + + +-- Populate parameters table +INSERT INTO parameters (key, name, description, value, id, created_at, updated_at) VALUES ('auth.password_expiration_window', 'Local Auth Password Expiry Window', 'This configuration parameter sets the local auth password expiry window for users that have valid auth secrets. Values for this configuration must follow the duration specification of ISO-8601.', '{"duration": "P90D"}', 1, current_timestamp, current_timestamp); +INSERT INTO parameters (key, name, description, value, id, created_at, updated_at) VALUES ('neo4j.configuration', 'Neo4j Configuration Parameters', 'This configuration parameter sets the BatchWriteSize and the BatchFlushSize for Neo4J.', '{"batch_write_size": 20000, "write_flush_size": 100000}', 2, current_timestamp, current_timestamp); diff --git a/cmd/api/src/database/migration/migrations/v5.1.1.sql b/cmd/api/src/database/migration/migrations/v5.1.1.sql index 1f8f287c5f..3f920ce35b 100644 --- a/cmd/api/src/database/migration/migrations/v5.1.1.sql +++ b/cmd/api/src/database/migration/migrations/v5.1.1.sql @@ -14,6 +14,6 @@ -- -- SPDX-License-Identifier: Apache-2.0 -INSERT INTO asset_groups (name, tag, system_group) -SELECT 'Owned', 'owned', true +INSERT INTO asset_groups (name, tag, system_group, created_at, updated_at) +SELECT 'Owned', 'owned', true, current_timestamp, current_timestamp WHERE NOT EXISTS (SELECT 1 FROM asset_groups WHERE tag='owned') diff --git a/cmd/api/src/database/migration/migrations/v5.15.0.sql b/cmd/api/src/database/migration/migrations/v5.15.0.sql new file mode 100644 index 0000000000..2461b8d6f6 --- /dev/null +++ b/cmd/api/src/database/migration/migrations/v5.15.0.sql @@ -0,0 +1,63 @@ +-- Copyright 2024 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 + +-- Feature Flags +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'adcs', 'Enable collection and processing of Active Directory Certificate Services Data', 'Enables the ability to collect, analyze, and explore Active Directory Certificate Services data and previews new attack paths.', false, true) ON CONFLICT DO NOTHING; +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'clear_graph_data', 'Clear Graph Data', 'Enables the ability to delete all nodes and edges from the graph database.', true, false) ON CONFLICT DO NOTHING; +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'risk_exposure_new_calculation', 'Use new tier zero risk exposure calculation', 'Enables the use of new tier zero risk exposure metatree metrics.', false, false) ON CONFLICT DO NOTHING; +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) VALUES (current_timestamp, current_timestamp, 'fedramp_eula', 'FedRAMP EULA', 'Enables showing the FedRAMP EULA on every login. (Enterprise only)', false, false) ON CONFLICT DO NOTHING; + +-- Note - order matters permissions and roles ops must come before roles permissions ops +-- Permissions +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('saved_queries', 'Read', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('saved_queries', 'Write', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('clients', 'Read', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('db', 'Wipe', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; +INSERT INTO permissions (authority, name, created_at, updated_at) VALUES ('graphdb', 'Mutate', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; + +-- Roles +INSERT INTO roles (name, description, created_at, updated_at) VALUES ('Power User', 'Can upload data, manage clients, and perform any action a User can', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING; + +-- Roles Permissions +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Write')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'db' and permissions.name = 'Wipe')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Administrator'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' AND permissions.name = 'Mutate')) ON CONFLICT DO NOTHING; + +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Write')) ON CONFLICT DO NOTHING; +-- Swap user clients manage for clients read permission +DELETE FROM roles_permissions WHERE role_id = (SELECT id FROM roles WHERE roles.name = 'User') AND permission_id = (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Manage'); +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'User'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; + +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'CreateToken')) ON CONFLICT DO NOTHING; + +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'ReadAppConfig')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'app' and permissions.name = 'WriteAppConfig')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'GenerateReport')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'risks' and permissions.name = 'ManageRisks')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'CreateToken')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'auth' and permissions.name = 'ManageSelf')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Manage')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'clients' and permissions.name = 'Tasking')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'collection' and permissions.name = 'ManageJobs')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Write')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Read')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Write')) ON CONFLICT DO NOTHING; +INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Power User'), (SELECT id FROM permissions WHERE permissions.authority = 'graphdb' AND permissions.name = 'Mutate')) ON CONFLICT DO NOTHING; diff --git a/cmd/api/src/database/migration/migrations/v5.4.0.sql b/cmd/api/src/database/migration/migrations/v5.4.0.sql index 3195e30eeb..5ee5543cb7 100644 --- a/cmd/api/src/database/migration/migrations/v5.4.0.sql +++ b/cmd/api/src/database/migration/migrations/v5.4.0.sql @@ -32,4 +32,4 @@ ADD COLUMN IF NOT EXISTS certtemplates BIGINT DEFAULT 0; DELETE FROM saved_queries WHERE - user_id = '00000000-0000-0000-0000-000000000000' + user_id = '00000000-0000-0000-0000-000000000000'; diff --git a/cmd/api/src/database/migration/stepwise.go b/cmd/api/src/database/migration/stepwise.go index ac24b56067..bada240ce9 100644 --- a/cmd/api/src/database/migration/stepwise.go +++ b/cmd/api/src/database/migration/stepwise.go @@ -145,7 +145,7 @@ func (s *Migrator) RequiresMigration() (bool, error) { } } -// executeStepwiseMigrations will run all necessary migrations for a deployment. +// ExecuteStepwiseMigrations will run all necessary migrations for a deployment. // It begins by checking if migration schema exists. If it does not, we assume the // deployment is a new installation, otherwise we assume it may have migration updates. // @@ -156,7 +156,7 @@ func (s *Migrator) RequiresMigration() (bool, error) { // and then build a manifest starting after the last successful version // // Once schema is verified and a manifest is created, we run ExecuteMigrations. -func (s *Migrator) executeStepwiseMigrations() error { +func (s *Migrator) ExecuteStepwiseMigrations() error { var ( manifest Manifest lastMigration model.Migration diff --git a/cmd/api/src/migrations/graph.go b/cmd/api/src/migrations/graph.go index 004b8202dc..1b51aeffee 100644 --- a/cmd/api/src/migrations/graph.go +++ b/cmd/api/src/migrations/graph.go @@ -91,10 +91,10 @@ func GetMigrationData(ctx context.Context, db graph.Database) (version.Version, log.Warnf("Unable to get Major property from migration data node: %v", err) return currentMigration, ErrNoMigrationData } else if minor, err := node.Properties.Get("Minor").Int(); err != nil { - log.Warnf("unable to get Major property from migration data node: %v", err) + log.Warnf("unable to get Minor property from migration data node: %v", err) return currentMigration, ErrNoMigrationData } else if patch, err := node.Properties.Get("Patch").Int(); err != nil { - log.Warnf("unable to get Major property from migration data node: %v", err) + log.Warnf("unable to get Patch property from migration data node: %v", err) return currentMigration, ErrNoMigrationData } else { currentMigration.Major = major diff --git a/cmd/api/src/model/appcfg/flag.go b/cmd/api/src/model/appcfg/flag.go index aa7e682914..4e4d2afe48 100644 --- a/cmd/api/src/model/appcfg/flag.go +++ b/cmd/api/src/model/appcfg/flag.go @@ -22,6 +22,7 @@ import ( "github.com/specterops/bloodhound/src/model" ) +// AvailableFlags has been removed and the db feature_flags table is the source of truth. Feature flag defaults should be added via migration *.sql files. const ( FeatureButterflyAnalysis = "butterfly_analysis" FeatureEnableSAMLSSO = "enable_saml_sso" @@ -37,97 +38,6 @@ const ( FeatureDarkMode = "dark_mode" ) -// AvailableFlags returns a FeatureFlagSet of expected feature flags. Feature flag defaults introduced here will become the initial -// default value of the feature flag once it is inserted into the database. -func AvailableFlags() FeatureFlagSet { - return FeatureFlagSet{ - FeatureButterflyAnalysis: { - Key: FeatureButterflyAnalysis, - Name: "Enhanced Asset Inbound-Outbound Exposure Analysis", - Description: "Enables more extensive analysis of attack path findings that allows BloodHound to help the user prioritize remediation of the most exposed assets.", - Enabled: false, - UserUpdatable: false, - }, - FeaturePGMigrationDualIngest: { - Key: FeaturePGMigrationDualIngest, - Name: "PostgreSQL Migration Dual Ingest", - Description: "Enables dual ingest pathing for both Neo4j and PostgreSQL.", - Enabled: false, - UserUpdatable: false, - }, - FeatureEnableSAMLSSO: { - Key: FeatureEnableSAMLSSO, - Name: "SAML Single Sign-On Support", - Description: "Enables SSO authentication flows and administration panels to third party SAML identity providers.", - Enabled: true, - UserUpdatable: false, - }, - FeatureScopeCollectionByOU: { - Key: FeatureScopeCollectionByOU, - Name: "Enable SharpHound OU Scoped Collections", - Description: "Enables scoping SharpHound collections to specific lists of OUs.", - Enabled: true, - UserUpdatable: false, - }, - FeatureAzureSupport: { - Key: FeatureAzureSupport, - Name: "Enable Azure Support", - Description: "Enables Azure support.", - Enabled: true, - UserUpdatable: false, - }, - FeatureReconciliation: { - Key: FeatureReconciliation, - Name: "Reconciliation", - Description: "Enables Reconciliation", - Enabled: true, - UserUpdatable: false, - }, - FeatureEntityPanelCaching: { - Key: FeatureEntityPanelCaching, - Name: "Enable application level caching", - Description: "Enables the use of application level caching for entity panel queries", - Enabled: true, - UserUpdatable: false, - }, - FeatureAdcs: { - Key: FeatureAdcs, - Name: "Enable collection and processing of Active Directory Certificate Services Data", - Description: "Enables the ability to collect, analyze, and explore Active Directory Certificate Services data and previews new attack paths.", - Enabled: false, - UserUpdatable: false, - }, - FeatureClearGraphData: { - Key: FeatureClearGraphData, - Name: "Clear Graph Data", - Description: "Enables the ability to delete all nodes and edges from the graph database.", - Enabled: true, - UserUpdatable: false, - }, - FeatureRiskExposureNewCalculation: { - Key: FeatureRiskExposureNewCalculation, - Name: "Use new tier zero risk exposure calculation", - Description: "Enables the use of new tier zero risk exposure metatree metrics.", - Enabled: false, - UserUpdatable: false, - }, - FeatureFedRAMPEULA: { - Key: FeatureFedRAMPEULA, - Name: "FedRAMP EULA", - Description: "Enables showing the FedRAMP EULA on every login. (Enterprise only)", - Enabled: false, - UserUpdatable: false, - }, - FeatureDarkMode: { - Key: FeatureDarkMode, - Name: "Dark Mode", - Description: "Allows users to enable or disable dark mode via a toggle in the settings menu", - Enabled: false, - UserUpdatable: true, - }, - } -} - // FeatureFlag defines the most basic details of what a feature flag must contain to be actionable. Feature flags should be // self-descriptive as many use-cases will involve iterating over all available flags to display them back to the // end-user.