From 9448adf5a7f3a995f29e9f9babc416b4e6008030 Mon Sep 17 00:00:00 2001 From: database-tycoon Date: Sun, 22 Feb 2026 18:50:35 -0500 Subject: [PATCH] feat: Complete v0.3 release - tests, CI validation, config fixes, Terraform boilerplate, changelog - Add 154 unit tests for validate_config.py and generate_terraform.py (543 total) - Add validate-config step to CI pipeline (runs before tests) - Add validate-config pre-commit hook for YAML changes - Fix user.yaml: correct default_role references (ANALYST__B_ROLE -> COMPANY_USERS__B_ROLE, DLT_INGESTION_ROLE__B_ROLE -> DLT_STRIPE_ROLE__B_ROLE) - Add terraform-snowflake-boilerplate/ with generated example output - Rewrite CHANGELOG.md with clean semantic versioning format - Bump version to 0.3.0 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 3 + .pre-commit-config.yaml | 8 + CHANGELOG.md | 436 +--- pyproject.toml | 2 +- snowddl/user.yaml | 4 +- terraform-snowflake-boilerplate/README.md | 39 + terraform-snowflake-boilerplate/databases.tf | 34 + terraform-snowflake-boilerplate/grants.tf | 1791 +++++++++++++++++ terraform-snowflake-boilerplate/main.tf | 11 + terraform-snowflake-boilerplate/policies.tf | 194 ++ .../resource_monitors.tf | 107 + terraform-snowflake-boilerplate/roles.tf | 374 ++++ terraform-snowflake-boilerplate/users.tf | 64 + terraform-snowflake-boilerplate/warehouses.tf | 231 +++ tests/test_generate_terraform.py | 703 +++++++ tests/test_validate_config.py | 947 +++++++++ uv.lock | 2 +- 17 files changed, 4565 insertions(+), 385 deletions(-) create mode 100644 terraform-snowflake-boilerplate/README.md create mode 100644 terraform-snowflake-boilerplate/databases.tf create mode 100644 terraform-snowflake-boilerplate/grants.tf create mode 100644 terraform-snowflake-boilerplate/main.tf create mode 100644 terraform-snowflake-boilerplate/policies.tf create mode 100644 terraform-snowflake-boilerplate/resource_monitors.tf create mode 100644 terraform-snowflake-boilerplate/roles.tf create mode 100644 terraform-snowflake-boilerplate/users.tf create mode 100644 terraform-snowflake-boilerplate/warehouses.tf create mode 100644 tests/test_generate_terraform.py create mode 100644 tests/test_validate_config.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e543f0..741fcdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Install dependencies run: uv sync --all-extras --dev + - name: Validate YAML configs + run: uv run validate-config + - name: Run tests run: uv run pytest -v --tb=short env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2912cc7..9898b01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,6 +37,14 @@ repos: pass_filenames: false always_run: false + - id: validate-config + name: Validate SnowDDL YAML Configs + entry: uv run validate-config --quiet + language: system + files: ^snowddl/.*\.yaml$ + pass_filenames: false + always_run: false + - repo: https://github.com/psf/black rev: 24.4.2 # Updated from 23.12.1 - stable version compatible with Python 3.12.5 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c8fb5..409f017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,389 +1,63 @@ # Changelog -All notable changes to the SnowTower SnowDDL project will be documented in this file. +All notable changes to SnowTower are documented in this file. +Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.3.0] - 2026-02-22 -## [2025-12-03] - Infrastructure Update +### Added +- `validate-config` command: Pre-deployment YAML validation with cross-reference integrity checks +- `generate-terraform` command: Convert SnowDDL YAML to Terraform HCL with import blocks +- Integration test suite: 58 tests covering CLI, authentication, YAML validation, deployment workflows +- Secrets scanning: detect-secrets pre-commit hook + TruffleHog CI job +- `validate-config` pre-commit hook for automatic YAML validation on commit +- `validate-config` CI step for validation on every push/PR +- Terraform boilerplate example output in `terraform-snowflake-boilerplate/` +- Unit tests for `validate_config.py` and `generate_terraform.py` ### Changed -- Merge pull request #75 from Database-Tycoon/fix/dlt-table-stage-permissions -- fix: Add schema_owner to DBT business role for PROJ_STRIPE access -- docs: update changelog for deployment 79410355 -- Merge pull request #74 from Database-Tycoon/fix/dlt-table-stage-permissions -- chore: Remove plan generation from PR validation workflow -- fix: Change DEV_CAROL to non-sandbox to match existing PERMANENT schema -- docs: update changelog for deployment 7559b03e -- Merge pull request #73 from Database-Tycoon/fix/dlt-table-stage-permissions -- fix: Add schema_owner to DLT business role for table stage access -- docs: Add note about DLT schema owner role requirement -- docs: update changelog for deployment 23fe7d97 -- Merge pull request #72 from Database-Tycoon/fix/ci-workflow-exit-codes -- fix: Remove obsolete transfer_ownership_to_dbt step from CI -- fix: Use uv run for transfer_ownership_to_dbt.py -- fix: Handle snowddl-apply exit code 8 as success -- fix: Handle snowddl-plan exit codes in all deployment steps - -### Infrastructure Changes -- fix: Add schema_owner to DBT business role for PROJ_STRIPE access (a542757) -- fix: Change DEV_CAROL to non-sandbox to match existing PERMANENT schema (d29f9ff) -- fix: Add schema_owner to DLT business role for table stage access (da7de9f) -- docs: Add note about DLT schema owner role requirement (dc81bf1) - -### Deployment Info -- **Commit:** 50a30bf6 -- **Timestamp:** 2025-12-03 12:45:03 UTC -- **Triggered by:** example-user - - - -## [2025-12-03] - Infrastructure Update - -### Changed -- Merge pull request #74 from Database-Tycoon/fix/dlt-table-stage-permissions -- chore: Remove plan generation from PR validation workflow -- fix: Change DEV_CAROL to non-sandbox to match existing PERMANENT schema -- docs: update changelog for deployment 7559b03e -- Merge pull request #73 from Database-Tycoon/fix/dlt-table-stage-permissions -- fix: Add schema_owner to DLT business role for table stage access -- docs: Add note about DLT schema owner role requirement -- docs: update changelog for deployment 23fe7d97 -- Merge pull request #72 from Database-Tycoon/fix/ci-workflow-exit-codes -- fix: Remove obsolete transfer_ownership_to_dbt step from CI -- fix: Use uv run for transfer_ownership_to_dbt.py -- fix: Handle snowddl-apply exit code 8 as success -- fix: Handle snowddl-plan exit codes in all deployment steps -- fix: Handle snowddl-plan non-zero exit codes in deployment workflow -- docs: update changelog for deployment 73736954 - -### Infrastructure Changes -- fix: Change DEV_CAROL to non-sandbox to match existing PERMANENT schema (d29f9ff) -- fix: Add schema_owner to DLT business role for table stage access (da7de9f) -- docs: Add note about DLT schema owner role requirement (dc81bf1) - -### Deployment Info -- **Commit:** 79410355 -- **Timestamp:** 2025-12-03 12:14:32 UTC -- **Triggered by:** example-user - - - -## [2025-12-03] - Infrastructure Update - -### Changed -- Merge pull request #73 from Database-Tycoon/fix/dlt-table-stage-permissions -- fix: Add schema_owner to DLT business role for table stage access -- docs: Add note about DLT schema owner role requirement -- docs: update changelog for deployment 23fe7d97 -- Merge pull request #72 from Database-Tycoon/fix/ci-workflow-exit-codes -- fix: Remove obsolete transfer_ownership_to_dbt step from CI -- fix: Use uv run for transfer_ownership_to_dbt.py -- fix: Handle snowddl-apply exit code 8 as success -- fix: Handle snowddl-plan exit codes in all deployment steps -- fix: Handle snowddl-plan non-zero exit codes in deployment workflow -- docs: update changelog for deployment 73736954 -- Merge pull request #71 from Database-Tycoon/feature/eliminate-schema-drift -- feat: Enable SnowDDL schema management and fix directory structure -- docs: Add SnowDDL knowledge base and fix test script -- fix: Resolve all test failures and add release checklist -- chore: Prepare for v0.1 release -- chore: Add keys/ to .gitignore and cleanup local artifacts -- fix: Correct docs path in help_cli.py -- feat: Remove web/Streamlit features for v0.1 release -- docs: restructure documentation and add LLM agent configuration - -### Infrastructure Changes -- fix: Add schema_owner to DLT business role for table stage access (da7de9f) -- docs: Add note about DLT schema owner role requirement (dc81bf1) -- feat: Enable SnowDDL schema management and fix directory structure (8d49e44) -- feat: Add schema.yaml files for all databases to eliminate schema drift (8fc2140) - -### Deployment Info -- **Commit:** 7559b03e -- **Timestamp:** 2025-12-03 11:46:16 UTC -- **Triggered by:** example-user - - - -## [2025-11-30] - Infrastructure Update - -### Changed -- Merge pull request #72 from Database-Tycoon/fix/ci-workflow-exit-codes -- fix: Remove obsolete transfer_ownership_to_dbt step from CI -- fix: Use uv run for transfer_ownership_to_dbt.py -- fix: Handle snowddl-apply exit code 8 as success -- fix: Handle snowddl-plan exit codes in all deployment steps -- fix: Handle snowddl-plan non-zero exit codes in deployment workflow -- docs: update changelog for deployment 73736954 -- Merge pull request #71 from Database-Tycoon/feature/eliminate-schema-drift -- feat: Enable SnowDDL schema management and fix directory structure -- docs: Add SnowDDL knowledge base and fix test script -- fix: Resolve all test failures and add release checklist -- chore: Prepare for v0.1 release -- chore: Add keys/ to .gitignore and cleanup local artifacts -- fix: Correct docs path in help_cli.py -- feat: Remove web/Streamlit features for v0.1 release -- docs: restructure documentation and add LLM agent configuration -- docs: Add comprehensive next steps guide for schema drift elimination -- feat: Add schema.yaml files for all databases to eliminate schema drift -- docs: Add comprehensive CHANGELOG.md for 0.1 release -- fix: Remove 6 non-working command references - -### Infrastructure Changes -- feat: Enable SnowDDL schema management and fix directory structure (8d49e44) -- feat: Add schema.yaml files for all databases to eliminate schema drift (8fc2140) -- feat: Add IP 192.0.2.10 to bi_tool_network_policy (a0d86c5) - -### Deployment Info -- **Commit:** 23fe7d97 -- **Timestamp:** 2025-11-30 23:33:51 UTC -- **Triggered by:** example-user - - - -## [2025-11-30] - Infrastructure Update +- Consolidated `.claude` resources: ~2,900 lines across 13 files reduced to ~580 lines across 5 files +- Rewrote 4 Claude Code skills as concise task guides (~60-116 lines each) +- Enhanced root `CLAUDE.md` as single source of truth with SnowDDL knowledge base +- Version bumped to 0.3.0 + +### Fixed +- Mismatched `default_role` references in `user.yaml` (ANALYST__B_ROLE, DLT_INGESTION_ROLE__B_ROLE) +- Stale references to nonexistent `.claude/agents/` directory +- Dead links to deleted `docs/llm-context/` directory + +### Removed +- `docs/llm-context/` directory (4 files) - consolidated into root CLAUDE.md +- Redundant skill files (developer README/CHANGELOG, maintainer PROJECT_STRUCTURE, skills README) + +## [0.2.0] - 2025-10-15 + +### Added +- Claude Code skills: snowtower-user, snowtower-admin, snowtower-developer, snowtower-maintainer +- CI/CD workflows: lint, test, secrets scanning via GitHub Actions +- PR template and issue templates +- CONTRIBUTING.md with development guidelines +- Pre-commit hooks for code formatting and YAML validation +- ASCII banner command (`snowtower-banner`) ### Changed -- Merge pull request #71 from Database-Tycoon/feature/eliminate-schema-drift -- feat: Enable SnowDDL schema management and fix directory structure -- docs: Add SnowDDL knowledge base and fix test script -- fix: Resolve all test failures and add release checklist -- chore: Prepare for v0.1 release -- chore: Add keys/ to .gitignore and cleanup local artifacts -- fix: Correct docs path in help_cli.py -- feat: Remove web/Streamlit features for v0.1 release -- docs: restructure documentation and add LLM agent configuration -- docs: Add comprehensive next steps guide for schema drift elimination -- feat: Add schema.yaml files for all databases to eliminate schema drift -- docs: Add comprehensive CHANGELOG.md for 0.1 release -- fix: Remove 6 non-working command references -- Merge pull request #59 from Database-Tycoon/fix/automatic-ownership-transfer-in-ci -- fix: add automatic ownership transfer to deployment workflow -- Merge pull request #58 from Database-Tycoon/fix/lightdash-network-policy-deployment -- fix: Add ownership investigation and transfer tools -- fix: Prevent dbt permission loss with deploy-safe wrapper -- feat: Add IP 192.0.2.10 to bi_tool_network_policy -- docs: Add Lightdash network policy deployment documentation - -### Infrastructure Changes -- feat: Enable SnowDDL schema management and fix directory structure (8d49e44) -- feat: Add schema.yaml files for all databases to eliminate schema drift (8fc2140) -- feat: Add IP 192.0.2.10 to bi_tool_network_policy (a0d86c5) -- Add RSA public key for CAROL user (3d8fcac) -- fix: Add CREATE SCHEMA privilege to DBT_STRIPE_ROLE for SOURCE_STRIPE (3a59c83) - -### Deployment Info -- **Commit:** 73736954 -- **Timestamp:** 2025-11-30 20:16:38 UTC -- **Triggered by:** example-user - - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.1.0] - 2025-11-21 - -### 🎉 Initial Release - -The first official release of SnowTower - a unified Snowflake infrastructure management platform with comprehensive CLI commands and Infrastructure as Code capabilities. - -### ✨ Major Features - -#### Infrastructure as Code -- **SnowDDL Integration**: Complete declarative infrastructure management via YAML configurations -- **Safe Deployment Workflow**: `uv run deploy-safe` ensures schema grants are always applied after infrastructure changes -- **Intelligent Plan Filtering** ⭐: Automatic suppression of expected schema grant drift in PR reviews (see [#66](https://github.com/Database-Tycoon/snowtower/pull/66)) - - Eliminates hundreds of REVOKE statements from plan output - - Makes actual infrastructure changes crystal clear - - Preserves full audit trail in collapsible sections - - Integrated into CI/CD workflows - -#### User Management -- **Complete User Lifecycle**: Interactive and non-interactive user creation with `uv run manage-users` -- **Dual Authentication**: RSA key-pair (primary) + encrypted password (fallback) for all users -- **MFA Compliance Tracking**: Ready for Snowflake's 2025-2026 mandatory MFA rollout -- **Bulk Operations**: Password generation, CSV import, key rotation - -#### Resource Management -- **Warehouse Management**: Resize, auto-suspend, cost optimization with `uv run manage-warehouses` -- **Cost Optimization**: Analysis and recommendations with `uv run manage-costs` -- **Security Auditing**: Comprehensive security checks with `uv run manage-security` -- **Backup & Restore**: Configuration snapshots with `uv run manage-backup` - -#### Monitoring & Observability -- **Health Checks**: System health monitoring with `uv run monitor-health` -- **Audit Logs**: Complete audit trail with `uv run monitor-audit` -- **Operational Metrics**: Performance and usage metrics with `uv run monitor-metrics` - -#### CI/CD Automation -- **GitHub Actions Workflows**: Automated PR validation and production deployment -- **Safety Gates**: Pre-deployment validation, security scanning, health checks -- **Schema Grant Protection**: Automatic application of schema grants after every deployment -- **Emergency Rollback**: Snapshot-based rollback capabilities - -### 🔐 Security - -- **MFA Enforcement**: Timeline-ready for Snowflake's mandatory MFA (March 2026) -- **RSA Key Authentication**: Prioritized for all service accounts -- **Encrypted Passwords**: Fernet encryption for all password storage -- **Network Policies**: IP restrictions for human users (192.0.2.10/32) -- **Emergency Access**: STEPHEN_RECOVERY account preserved without network restrictions -- **Audit Trail**: Complete logging of all infrastructure changes - -### 📚 Documentation - -- **Comprehensive README**: User-focused onboarding and administrator guide -- **Management Commands Reference**: Complete CLI command documentation -- **Quick Start Guide**: 5-minute setup for new users -- **Schema Grants Workaround**: Detailed explanation of SnowDDL's SCHEMA exclusion pattern -- **Security Guides**: Authentication setup, MFA compliance, RSA key generation -- **0.1 Release Review**: Comprehensive pre-release audit and recommendations - -### 🛠️ CLI Commands - -**25+ Working Commands** organized in 7 categories: - -#### Core SnowDDL Operations (5 commands) -- `snowddl-plan` - Preview infrastructure changes with intelligent filtering -- `snowddl-apply` - Apply infrastructure changes -- `snowddl-validate` - Validate YAML configurations -- `snowddl-diff` - Show differences -- `deploy-safe` - Safe deployment with automatic schema grants ⭐ - -#### User Management (1 command with subcommands) -- `manage-users` - Complete user lifecycle (create, list, update, delete, validate, etc.) - -#### Resource Management (6 commands) -- `manage-warehouses` - Warehouse operations and optimization -- `manage-costs` - Cost analysis and optimization -- `manage-security` - Security auditing -- `manage-backup` - Configuration backup/restore -- `apply-schema-grants` - Apply schema-level grants ⭐ -- `validate-schema-grants` - Validate schema grant consistency - -#### Monitoring (3 commands) -- `monitor-health` - System health checks -- `monitor-audit` - Audit trail analysis -- `monitor-metrics` - Operational metrics - -#### Web Interface (2 commands) -- `web` - Launch Streamlit dashboard -- `deploy-streamlit` - Deploy Streamlit apps to Snowflake - -#### Utilities (4 commands) -- `util-generate-key` - Generate Fernet encryption key -- `util-diagnose-auth` - Diagnose authentication issues -- `util-fix-auth` - Fix authentication problems -- `generate-rsa-batch` - Batch RSA key generation - -#### Automation (2 commands) -- `github-to-snowddl` - Convert GitHub issues to SnowDDL PRs -- `process-access-request` - Process user access requests - -#### Documentation (2 commands) -- `docs-serve` - Serve documentation locally -- `docs-build` - Build documentation - -#### New in 0.1 (1 command) -- `filter-plan` - Intelligent plan output filtering ⭐ - -### 🐛 Fixed - -- **Schema Grant Drift Noise**: Intelligent filtering eliminates hundreds of REVOKE statements from PR reviews -- **Missing Command References**: Removed 6 non-existent commands from registry - - `test-s3-deployment` - - `sync-s3-configs` - - `test-streamlit-local` - - `test-streamlit-deployed` - - `deploy-streamlit-safe` - - `detect-streamlit-errors` -- **Documentation Accuracy**: Updated command documentation to reflect actual implementations - -### ⚠️ Known Limitations - -#### Web UI -- **Status**: Has known errors and issues -- **Recommendation**: Use CLI commands instead -- **Launch Command**: `uv run web` works but may have runtime errors -- **Roadmap**: Full web UI refactoring planned for 0.2 release - -#### Test Coverage -- **Current**: ~40% (filter tests only) -- **Target**: >80% by 0.2 release -- **Status**: Integration tests planned but not yet implemented - -#### Streamlit Testing -- Basic validation available via `validate-streamlit` -- Advanced testing commands deferred to future release - -### 🚀 Deployment - -#### Prerequisites -- Python 3.10+ -- UV package manager -- Snowflake account with ACCOUNTADMIN role -- RSA key pair for authentication - -#### Quick Start -```bash -# 1. Clone and install -git clone https://github.com/Database-Tycoon/snowtower.git -cd snowtower-snowddl -uv sync - -# 2. Configure authentication -cp .env.example .env -# Edit .env with your credentials - -# 3. Preview changes -uv run snowddl-plan - -# 4. Deploy safely -uv run deploy-safe -``` - -### 📊 Project Statistics - -- **Active Users**: 13 configured (7 human, 5 service accounts) -- **Databases Managed**: 6 production databases -- **Warehouses**: 8 with auto-suspend -- **Resource Monitors**: 7 active cost monitors -- **Security Policies**: Network and authentication policies enforced -- **Test Coverage**: 13 passing tests for plan filtering - -### 🔄 Migration Guide - -This is the initial release, so no migration is required. For users of the pre-release versions: - -1. **Update Dependencies**: Run `uv sync` to install all dependencies -2. **Review Commands**: Some commands have been removed - see Fixed section above -3. **Update Workflows**: CI/CD workflows now include intelligent filtering -4. **Review Documentation**: New comprehensive guides in `docs/` directory - -### 🙏 Acknowledgments - -- Built with [SnowDDL](https://github.com/littleK0i/SnowDDL) by littleK0i -- Powered by [UV](https://github.com/astral-sh/uv) package manager by Astral -- Infrastructure automation via [GitHub Actions](https://github.com/features/actions) - -### 📝 Notes - -- **Recommended Deployment**: Always use `uv run deploy-safe` instead of `snowddl-apply` -- **Schema Grants**: See `docs/SCHEMA_GRANTS_WORKAROUND.md` for critical information -- **MFA Timeline**: Enable MFA for human users before March 2026 deadline -- **RSA Keys**: Primary authentication method for all service accounts - ---- - -## [Unreleased] - -### Planned for 0.2.0 -- Complete web UI refactoring and error fixes -- Integration test suite for CLI commands -- Increased test coverage (>80%) -- Advanced Streamlit testing commands -- API documentation generation -- Multi-account support exploration - ---- - -**Full Changelog**: https://github.com/Database-Tycoon/snowtower/commits/v0.1.0 +- Consolidated from dual-repo to single unified platform +- Updated README with badges and improved documentation + +## [0.1.0] - 2025-08-01 + +### Added +- Initial release of SnowTower enterprise Snowflake infrastructure management +- SnowDDL YAML-based infrastructure definitions (users, roles, warehouses, policies) +- User management with Fernet encryption and RSA key authentication +- OOP framework for SnowDDL operations (`snowddl_core`) +- Management CLI: warehouses, costs, security, backup, users +- Monitoring: health, audit, metrics +- Deploy-safe wrapper with safety checks +- Network, authentication, password, and session policies +- Resource monitors with trigger configuration +- Comprehensive documentation (README, QUICKSTART, RSA_KEY_SETUP) +- MFA compliance tracking for Snowflake 2025-2026 rollout +- 25+ CLI commands across 7 categories + +**Full Changelog**: https://github.com/Database-Tycoon/SnowTower/commits/main diff --git a/pyproject.toml b/pyproject.toml index 07eb28a..69490c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "snowtower" -version = "0.2.0" +version = "0.3.0" description = "SnowTower - Snowflake Infrastructure Management Platform using Infrastructure as Code" readme = "README.md" requires-python = ">=3.10" diff --git a/snowddl/user.yaml b/snowddl/user.yaml index 031419d..c5e1ce8 100644 --- a/snowddl/user.yaml +++ b/snowddl/user.yaml @@ -24,7 +24,7 @@ JOHN_ANALYST: email: john.analyst@example.com first_name: John last_name: Analyst - default_role: ANALYST__B_ROLE + default_role: COMPANY_USERS__B_ROLE comment: Data analyst # ============================================================================= @@ -46,7 +46,7 @@ DLT_SERVICE: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0example... ...replace with your actual RSA public key... AQAB - default_role: DLT_INGESTION_ROLE__B_ROLE + default_role: DLT_STRIPE_ROLE__B_ROLE comment: Data ingestion service account # ============================================================================= diff --git a/terraform-snowflake-boilerplate/README.md b/terraform-snowflake-boilerplate/README.md new file mode 100644 index 0000000..07d9546 --- /dev/null +++ b/terraform-snowflake-boilerplate/README.md @@ -0,0 +1,39 @@ +# Terraform Snowflake Boilerplate + +Auto-generated Terraform HCL files from SnowDDL YAML configurations. + +## Usage + +These files were generated by: + +```bash +uv run generate-terraform --output ./terraform-snowflake-boilerplate/ +``` + +To regenerate after YAML changes: + +```bash +uv run generate-terraform --output ./terraform-snowflake-boilerplate/ +``` + +## Files + +| File | Contents | +|------|----------| +| `main.tf` | Provider configuration | +| `users.tf` | `snowflake_user` resources | +| `warehouses.tf` | `snowflake_warehouse` resources | +| `roles.tf` | `snowflake_account_role` resources (business + tech) | +| `grants.tf` | `snowflake_grant_*` resources | +| `policies.tf` | Network, authentication, password, session policies | +| `databases.tf` | `snowflake_database` resources | +| `resource_monitors.tf` | `snowflake_resource_monitor` resources | + +## Import Support + +Each resource includes an `import {}` block so `terraform plan` can import existing Snowflake objects rather than attempting to recreate them. + +## Important + +These files are a starting point. Review and customize before running `terraform apply`. +The Snowflake provider requires authentication configuration not included here. diff --git a/terraform-snowflake-boilerplate/databases.tf b/terraform-snowflake-boilerplate/databases.tf new file mode 100644 index 0000000..eb00b9f --- /dev/null +++ b/terraform-snowflake-boilerplate/databases.tf @@ -0,0 +1,34 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Database resources + +import { + to = snowflake_database.dev_example + id = "DEV_EXAMPLE" +} + +resource "snowflake_database" "dev_example" { + name = "DEV_EXAMPLE" + comment = "Sandbox database DEV_EXAMPLE" +} + +import { + to = snowflake_database.lightdash + id = "LIGHTDASH" +} + +resource "snowflake_database" "lightdash" { + name = "LIGHTDASH" + comment = "LightDash BI platform database for analytics transformations and dashboard content" +} + +import { + to = snowflake_database.omni + id = "OMNI" +} + +resource "snowflake_database" "omni" { + name = "OMNI" + comment = "Omni BI platform database for analytics transformations and custom reporting" +} diff --git a/terraform-snowflake-boilerplate/grants.tf b/terraform-snowflake-boilerplate/grants.tf new file mode 100644 index 0000000..78f75c8 --- /dev/null +++ b/terraform-snowflake-boilerplate/grants.tf @@ -0,0 +1,1791 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Grant resources + +resource "snowflake_grant_account_role" "admin_role_b_role_inherits_streamlit_viewer_t_role" { + role_name = "STREAMLIT_VIEWER__T_ROLE" + parent_role_name = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "admin_role_b_role_inherits_dlt_loader_role_t_role" { + role_name = "DLT_LOADER_ROLE__T_ROLE" + parent_role_name = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "admin_role_b_role_inherits_snowtower_users_t_role" { + role_name = "SNOWTOWER_USERS__T_ROLE" + parent_role_name = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "admin_role_b_role_inherits_stripe_t_role" { + role_name = "STRIPE__T_ROLE" + parent_role_name = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "admin_role_b_role_inherits_company_users_t_role" { + role_name = "COMPANY_USERS__T_ROLE" + parent_role_name = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "admin_role_b_role_wh_admin" { + account_role_name = "ADMIN_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ADMIN" + } +} + +resource "snowflake_grant_privileges_to_account_role" "admin_role_b_role_wh_main_warehouse" { + account_role_name = "ADMIN_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_account_role" "ai_ml_role_b_role_inherits_fabi_ai_t_role" { + role_name = "FABI_AI__T_ROLE" + parent_role_name = "AI_ML_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "ai_ml_role_b_role_inherits_copilot_access_role_t_role" { + role_name = "COPILOT_ACCESS_ROLE__T_ROLE" + parent_role_name = "AI_ML_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "ai_ml_role_b_role_wh_fabi_ai_warehouse" { + account_role_name = "AI_ML_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "FABI_AI_WAREHOUSE" + } +} + +resource "snowflake_grant_account_role" "bi_developer_role_b_role_inherits_streamlit_viewer_t_role" { + role_name = "STREAMLIT_VIEWER__T_ROLE" + parent_role_name = "BI_DEVELOPER_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "bi_developer_role_b_role_inherits_dbt_stripe_role_t_role" { + role_name = "DBT_STRIPE_ROLE__T_ROLE" + parent_role_name = "BI_DEVELOPER_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "bi_developer_role_b_role_inherits_bi_writer_tech_role_t_role" { + role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + parent_role_name = "BI_DEVELOPER_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "bi_developer_role_b_role_inherits_sigma_role_t_role" { + role_name = "SIGMA_ROLE__T_ROLE" + parent_role_name = "BI_DEVELOPER_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "bi_developer_role_b_role_wh_transforming" { + account_role_name = "BI_DEVELOPER_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_developer_role_b_role_wh_analytics_tool" { + account_role_name = "BI_DEVELOPER_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_developer_role_b_role_wh_main_warehouse" { + account_role_name = "BI_DEVELOPER_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_account_role" "company_users_b_role_inherits_streamlit_viewer_t_role" { + role_name = "STREAMLIT_VIEWER__T_ROLE" + parent_role_name = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_grant_account_role" "company_users_b_role_inherits_dlt_loader_role_t_role" { + role_name = "DLT_LOADER_ROLE__T_ROLE" + parent_role_name = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_grant_account_role" "company_users_b_role_inherits_stripe_t_role" { + role_name = "STRIPE__T_ROLE" + parent_role_name = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_grant_account_role" "company_users_b_role_inherits_company_users_t_role" { + role_name = "COMPANY_USERS__T_ROLE" + parent_role_name = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_grant_account_role" "company_users_b_role_inherits_dbt_stripe_role_t_role" { + role_name = "DBT_STRIPE_ROLE__T_ROLE" + parent_role_name = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_b_role_wh_main_warehouse" { + account_role_name = "COMPANY_USERS__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_b_role_wh_dlt" { + account_role_name = "COMPANY_USERS__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "DLT" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_b_role_wh_stripe" { + account_role_name = "COMPANY_USERS__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_b_role_wh_transforming" { + account_role_name = "COMPANY_USERS__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_b_role_wh_dev_wh" { + account_role_name = "COMPANY_USERS__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "DEV_WH" + } +} + +resource "snowflake_grant_account_role" "data_integration_role_b_role_inherits_estuary_t_role" { + role_name = "ESTUARY__T_ROLE" + parent_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "data_integration_role_b_role_inherits_fivetran_t_role" { + role_name = "FIVETRAN__T_ROLE" + parent_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "data_integration_role_b_role_inherits_fivetran_role_t_role" { + role_name = "FIVETRAN_ROLE__T_ROLE" + parent_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "data_integration_role_b_role_inherits_matillion_role_t_role" { + role_name = "MATILLION_ROLE__T_ROLE" + parent_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "data_integration_role_b_role_inherits_dlt_loader_role_t_role" { + role_name = "DLT_LOADER_ROLE__T_ROLE" + parent_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "data_integration_role_b_role_wh_estuary" { + account_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ESTUARY" + } +} + +resource "snowflake_grant_privileges_to_account_role" "data_integration_role_b_role_wh_fivetran" { + account_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "FIVETRAN" + } +} + +resource "snowflake_grant_privileges_to_account_role" "data_integration_role_b_role_wh_transforming" { + account_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "data_integration_role_b_role_wh_stripe" { + account_role_name = "DATA_INTEGRATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STRIPE" + } +} + +resource "snowflake_grant_account_role" "dbt_analytics_role_b_role_inherits_dbt_stripe_role_t_role" { + role_name = "DBT_STRIPE_ROLE__T_ROLE" + parent_role_name = "DBT_ANALYTICS_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "dbt_analytics_role_b_role_inherits_dbt_training_t_role" { + role_name = "DBT_TRAINING__T_ROLE" + parent_role_name = "DBT_ANALYTICS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_analytics_role_b_role_wh_main_warehouse" { + account_role_name = "DBT_ANALYTICS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_analytics_role_b_role_wh_transforming" { + account_role_name = "DBT_ANALYTICS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_account_role" "dbt_analytics_role_b_role_owner_proj_stripe_proj_stripe" { + role_name = "PROJ_STRIPE__PROJ_STRIPE__OWNER__S_ROLE" + parent_role_name = "DBT_ANALYTICS_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "dlt_stripe_role_b_role_inherits_dlt_stripe_tech_role_t_role" { + role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + parent_role_name = "DLT_STRIPE_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_role_b_role_wh_dlt" { + account_role_name = "DLT_STRIPE_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "DLT" + } +} + +resource "snowflake_grant_account_role" "dlt_stripe_role_b_role_owner_source_stripe_stripe_why" { + role_name = "SOURCE_STRIPE__STRIPE_WHY__OWNER__S_ROLE" + parent_role_name = "DLT_STRIPE_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "infrastructure_automation_role_b_role_inherits_omni_infrastructure_automation_t_role" { + role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + parent_role_name = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "infrastructure_automation_role_b_role_inherits_tobiko_cloud_t_role" { + role_name = "TOBIKO_CLOUD__T_ROLE" + parent_role_name = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "infrastructure_automation_role_b_role_wh_analytics_tool" { + account_role_name = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "infrastructure_automation_role_b_role_wh_admin" { + account_role_name = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ADMIN" + } +} + +resource "snowflake_grant_account_role" "lightdash_business_role_b_role_inherits_lightdash_tech_role_t_role" { + role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + parent_role_name = "LIGHTDASH_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_business_role_b_role_wh_bi_tool" { + account_role_name = "LIGHTDASH_BUSINESS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "BI_TOOL" + } +} + +resource "snowflake_grant_account_role" "omni_business_role_b_role_inherits_omni_tech_role_t_role" { + role_name = "OMNI_TECH_ROLE__T_ROLE" + parent_role_name = "OMNI_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "omni_business_role_b_role_wh_analytics_tool" { + account_role_name = "OMNI_BUSINESS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_account_role" "recce_business_role_b_role_inherits_recce_tech_role_t_role" { + role_name = "RECCE_TECH_ROLE__T_ROLE" + parent_role_name = "RECCE_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "recce_business_role_b_role_wh_recce" { + account_role_name = "RECCE_BUSINESS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "RECCE" + } +} + +resource "snowflake_grant_account_role" "service_accounts_role_b_role_inherits_tobiko_cloud_t_role" { + role_name = "TOBIKO_CLOUD__T_ROLE" + parent_role_name = "SERVICE_ACCOUNTS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "service_accounts_role_b_role_wh_admin" { + account_role_name = "SERVICE_ACCOUNTS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ADMIN" + } +} + +resource "snowflake_grant_account_role" "streamlit_apps_role_b_role_inherits_streamlit_role_t_role" { + role_name = "STREAMLIT_ROLE__T_ROLE" + parent_role_name = "STREAMLIT_APPS_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "streamlit_apps_role_b_role_inherits_streamlit_towerapp_role_t_role" { + role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + parent_role_name = "STREAMLIT_APPS_ROLE__B_ROLE" +} + +resource "snowflake_grant_account_role" "streamlit_apps_role_b_role_inherits_streamlit_viewer_t_role" { + role_name = "STREAMLIT_VIEWER__T_ROLE" + parent_role_name = "STREAMLIT_APPS_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_apps_role_b_role_wh_streamlit_viewer_wh" { + account_role_name = "STREAMLIT_APPS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STREAMLIT_VIEWER_WH" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_apps_role_b_role_wh_main_warehouse" { + account_role_name = "STREAMLIT_APPS_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_account_role" "training_role_b_role_inherits_dbt_training_t_role" { + role_name = "DBT_TRAINING__T_ROLE" + parent_role_name = "TRAINING_ROLE__B_ROLE" +} + +resource "snowflake_grant_privileges_to_account_role" "training_role_b_role_wh_transforming_user" { + account_role_name = "TRAINING_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING_USER" + } +} + +resource "snowflake_grant_privileges_to_account_role" "training_role_b_role_wh_transforming" { + account_role_name = "TRAINING_ROLE__B_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_database_source_stripe_usage" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_database_analytics_tool_usage_create_schema" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_warehouse_transforming_usage" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_grant_warehouse_analytics_tool_usage" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_future_table_proj_stripe_select" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_future_table_source_stripe_select" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_future_table_analytics_tool_select_insert_update_delete_truncate" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "bi_writer_tech_role_t_role_future_view_analytics_tool_select" { + account_role_name = "BI_WRITER_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_alice_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_ALICE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_carol_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_CAROL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_dave_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_DAVE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_eve_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_EVE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_grace_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_GRACE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_bob_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_BOB" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_database_dev_frank_usage_create_schema" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "DEV_FRANK" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_schema_dev_alice_stripe_why_usage_modify_monitor_create_table_create_view_create_function_create_procedure_create_file_format_create_stage_create_sequence" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "MODIFY", "MONITOR", "CREATE TABLE", "CREATE VIEW", "CREATE FUNCTION", "CREATE PROCEDURE", "CREATE FILE FORMAT", "CREATE STAGE", "CREATE SEQUENCE"] + on_schema { + schema_name = "\"DEV_ALICE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_grant_warehouse_main_warehouse_usage" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_alice_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_ALICE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_carol_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_CAROL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_dave_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_DAVE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_eve_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_EVE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_grace_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_GRACE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_bob_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_BOB" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_stage_dev_frank_usage_read_write" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "DEV_FRANK" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_alice_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_ALICE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_carol_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_CAROL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_dave_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_DAVE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_eve_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_EVE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_grace_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_GRACE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_bob_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_BOB" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "company_users_t_role_future_table_dev_frank_select_insert_update_delete_truncate" { + account_role_name = "COMPANY_USERS__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "DEV_FRANK" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "copilot_access_role_t_role_grant_warehouse_main_warehouse_usage" { + account_role_name = "COPILOT_ACCESS_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_database_source_stripe_usage_create_schema" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_database_proj_stripe_usage_create_schema" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_database_analytics_tool_usage_create_schema" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_schema_proj_stripe_proj_stripe_usage_modify_monitor_create_table_create_view_create_function_create_procedure_create_file_format_create_stage_create_sequence" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE", "MODIFY", "MONITOR", "CREATE TABLE", "CREATE VIEW", "CREATE FUNCTION", "CREATE PROCEDURE", "CREATE FILE FORMAT", "CREATE STAGE", "CREATE SEQUENCE"] + on_schema { + schema_name = "\"PROJ_STRIPE.PROJ_STRIPE\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_grant_warehouse_transforming_usage" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_table_source_stripe_select" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_table_proj_stripe_select_insert_update_delete_truncate" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_table_analytics_tool_select_insert_update_delete_truncate" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_view_source_stripe_select" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_view_proj_stripe_select" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_stripe_role_t_role_future_view_analytics_tool_select" { + account_role_name = "DBT_STRIPE_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dbt_training_t_role_grant_warehouse_transforming_usage" { + account_role_name = "DBT_TRAINING__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_loader_role_t_role_grant_database_source_stripe_usage_create_schema" { + account_role_name = "DLT_LOADER_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_loader_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "DLT_LOADER_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_loader_role_t_role_future_table_source_stripe_select_insert_update_delete_truncate" { + account_role_name = "DLT_LOADER_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_loader_role_t_role_future_view_source_stripe_select" { + account_role_name = "DLT_LOADER_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_grant_database_source_stripe_usage_create_schema_monitor_modify" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA", "MONITOR", "MODIFY"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_grant_database_proj_stripe_usage_create_schema_monitor_modify" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA", "MONITOR", "MODIFY"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_grant_schema_source_stripe_stripe_why_usage_modify_monitor_create_table_create_view_create_file_format_create_stage_create_sequence" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "MODIFY", "MONITOR", "CREATE TABLE", "CREATE VIEW", "CREATE FILE FORMAT", "CREATE STAGE", "CREATE SEQUENCE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_grant_warehouse_dlt_usage_monitor_operate" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "MONITOR", "OPERATE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "DLT" + } +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_file_format_source_stripe_usage" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema_object { + object_type_plural = "FILE_FORMATS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_file_format_proj_stripe_usage" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema_object { + object_type_plural = "FILE_FORMATS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_sequence_source_stripe_usage" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema_object { + object_type_plural = "SEQUENCES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_sequence_proj_stripe_usage" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema_object { + object_type_plural = "SEQUENCES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_stage_source_stripe_usage_read_write" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_stage_proj_stripe_usage_read_write" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "READ", "WRITE"] + on_schema_object { + object_type_plural = "STAGES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_table_source_stripe_select_insert_update_delete_truncate_references" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_table_proj_stripe_select_insert_update_delete_truncate_references" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_view_source_stripe_select" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "dlt_stripe_tech_role_t_role_future_view_proj_stripe_select" { + account_role_name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "estuary_t_role_grant_warehouse_estuary_usage" { + account_role_name = "ESTUARY__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ESTUARY" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fabi_ai_t_role_grant_warehouse_fabi_ai_warehouse_usage" { + account_role_name = "FABI_AI__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "FABI_AI_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fivetran_t_role_grant_warehouse_fivetran_usage" { + account_role_name = "FIVETRAN__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "FIVETRAN" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fivetran_role_t_role_grant_database_source_stripe_usage_create_schema" { + account_role_name = "FIVETRAN_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fivetran_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "FIVETRAN_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fivetran_role_t_role_grant_warehouse_fivetran_usage" { + account_role_name = "FIVETRAN_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "FIVETRAN" + } +} + +resource "snowflake_grant_privileges_to_account_role" "fivetran_role_t_role_future_table_source_stripe_select_insert_update_delete_truncate" { + account_role_name = "FIVETRAN_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_grant_database_source_stripe_usage" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_grant_database_bi_tool_usage_create_schema" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "BI_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_grant_warehouse_bi_tool_usage" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "BI_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_table_proj_stripe_select" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_table_source_stripe_select" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_table_bi_tool_select_insert_update_delete_truncate" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "BI_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_view_proj_stripe_select" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_view_source_stripe_select" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "lightdash_tech_role_t_role_future_view_bi_tool_select" { + account_role_name = "LIGHTDASH_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "BI_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_grant_database_source_stripe_usage_create_schema" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_grant_database_proj_stripe_usage_create_schema" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_grant_warehouse_transforming_usage" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "TRANSFORMING" + } +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_future_table_source_stripe_select_insert_update_delete_truncate" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "matillion_role_t_role_future_table_proj_stripe_select_insert_update_delete_truncate" { + account_role_name = "MATILLION_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_grant_database_analytics_tool_usage_create_schema_monitor" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA", "MONITOR"] + on_account_object { + object_type = "DATABASE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_grant_database_proj_stripe_usage_create_schema_monitor" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA", "MONITOR"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_grant_warehouse_analytics_tool_usage_monitor_operate" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["USAGE", "MONITOR", "OPERATE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_grant_warehouse_admin_usage_monitor_operate" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["USAGE", "MONITOR", "OPERATE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ADMIN" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_future_table_analytics_tool_select_insert_update_delete_truncate" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_future_view_analytics_tool_select" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_infrastructure_automation_t_role_future_view_proj_stripe_select" { + account_role_name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_grant_database_analytics_tool_usage_create_schema" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_grant_warehouse_analytics_tool_usage" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_future_table_proj_stripe_select" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_future_table_analytics_tool_select_insert_update_delete_truncate" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "omni_tech_role_t_role_future_view_analytics_tool_select" { + account_role_name = "OMNI_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "recce_tech_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "RECCE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "recce_tech_role_t_role_grant_warehouse_recce_usage" { + account_role_name = "RECCE_TECH_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "RECCE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "recce_tech_role_t_role_future_table_proj_stripe_select" { + account_role_name = "RECCE_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "recce_tech_role_t_role_future_view_proj_stripe_select" { + account_role_name = "RECCE_TECH_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_grant_database_analytics_tool_usage" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_grant_warehouse_analytics_tool_usage" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ANALYTICS_TOOL" + } +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_future_table_proj_stripe_select" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_future_table_analytics_tool_select" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_future_view_proj_stripe_select" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "sigma_role_t_role_future_view_analytics_tool_select" { + account_role_name = "SIGMA_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "ANALYTICS_TOOL" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "snowtower_users_t_role_grant_warehouse_main_warehouse_usage" { + account_role_name = "SNOWTOWER_USERS__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "STREAMLIT_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_role_t_role_grant_warehouse_streamlit_viewer_wh_usage" { + account_role_name = "STREAMLIT_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STREAMLIT_VIEWER_WH" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_role_t_role_future_table_proj_stripe_select" { + account_role_name = "STREAMLIT_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_role_t_role_future_view_proj_stripe_select" { + account_role_name = "STREAMLIT_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_grant_database_proj_stripe_usage" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_grant_database_source_stripe_usage" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_grant_warehouse_streamlit_viewer_wh_usage" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STREAMLIT_VIEWER_WH" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_grant_warehouse_main_warehouse_usage" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "MAIN_WAREHOUSE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_future_table_proj_stripe_select" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_future_table_source_stripe_select" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_future_view_proj_stripe_select" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_towerapp_role_t_role_future_view_source_stripe_select" { + account_role_name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "streamlit_viewer_t_role_grant_warehouse_streamlit_viewer_wh_usage" { + account_role_name = "STREAMLIT_VIEWER__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "STREAMLIT_VIEWER_WH" + } +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_grant_database_source_stripe_usage_create_schema" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "SOURCE_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_grant_database_proj_stripe_usage_create_schema" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["USAGE", "CREATE SCHEMA"] + on_account_object { + object_type = "DATABASE" + object_name = "PROJ_STRIPE" + } +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_grant_schema_source_stripe_stripe_why_usage" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["USAGE"] + on_schema { + schema_name = "\"SOURCE_STRIPE.STRIPE_WHY\"" + } +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_future_table_source_stripe_select" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_future_table_proj_stripe_select" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "TABLES" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_future_view_source_stripe_select" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "SOURCE_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "stripe_t_role_future_view_proj_stripe_select" { + account_role_name = "STRIPE__T_ROLE" + privileges = ["SELECT"] + on_schema_object { + object_type_plural = "VIEWS" + in_database = "PROJ_STRIPE" + } + all_privileges = false + with_grant_option = false +} + +resource "snowflake_grant_privileges_to_account_role" "tobiko_cloud_t_role_grant_warehouse_admin_usage" { + account_role_name = "TOBIKO_CLOUD__T_ROLE" + privileges = ["USAGE"] + on_account_object { + object_type = "WAREHOUSE" + object_name = "ADMIN" + } +} diff --git a/terraform-snowflake-boilerplate/main.tf b/terraform-snowflake-boilerplate/main.tf new file mode 100644 index 0000000..149d2e2 --- /dev/null +++ b/terraform-snowflake-boilerplate/main.tf @@ -0,0 +1,11 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +terraform { + required_providers { + snowflake = { + source = "Snowflake-Labs/snowflake" + version = "~> 1.0" + } + } +} diff --git a/terraform-snowflake-boilerplate/policies.tf b/terraform-snowflake-boilerplate/policies.tf new file mode 100644 index 0000000..e8e1167 --- /dev/null +++ b/terraform-snowflake-boilerplate/policies.tf @@ -0,0 +1,194 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Policy resources (network, authentication, password, session) + +import { + to = snowflake_network_policy.analytics_network_policy + id = "ANALYTICS_NETWORK_POLICY" +} + +resource "snowflake_network_policy" "analytics_network_policy" { + name = "ANALYTICS_NETWORK_POLICY" + allowed_ip_list = ["3.211.115.21/32", "34.215.77.3/32", "44.199.154.198/32", "52.205.119.255/32", "52.33.167.28/32", "54.172.168.164/32", "54.213.140.155/32", "54.213.219.131/32"] + comment = "Restrict Omni BI platform service account to approved Omni IP addresses" +} + +import { + to = snowflake_network_policy.bi_tool_network_policy + id = "BI_TOOL_NETWORK_POLICY" +} + +resource "snowflake_network_policy" "bi_tool_network_policy" { + name = "BI_TOOL_NETWORK_POLICY" + allowed_ip_list = ["198.51.100.1/32", "198.51.100.2/32", "192.0.2.10/32"] + comment = "Restrict LightDash BI platform service account to approved LightDash IP addresses (US and EU regions)" +} + +import { + to = snowflake_network_policy.company_network_policy + id = "COMPANY_NETWORK_POLICY" +} + +resource "snowflake_network_policy" "company_network_policy" { + name = "COMPANY_NETWORK_POLICY" + allowed_ip_list = ["192.0.2.10/32"] + comment = "Restrict human user access to approved IP address only" +} + +import { + to = snowflake_authentication_policy.mfa_optional_policy + id = "MFA_OPTIONAL_POLICY" +} + +resource "snowflake_authentication_policy" "mfa_optional_policy" { + name = "MFA_OPTIONAL_POLICY" + comment = "Optional MFA policy for transition period before mandatory enforcement" + client_types = ["ALL"] + mfa_authentication_methods = ["PASSWORD", "SAML"] + mfa_enrollment = "OPTIONAL" +} + +import { + to = snowflake_authentication_policy.mfa_required_policy + id = "MFA_REQUIRED_POLICY" +} + +resource "snowflake_authentication_policy" "mfa_required_policy" { + name = "MFA_REQUIRED_POLICY" + comment = "Mandatory MFA policy for human users (TYPE=PERSON) to comply with Snowflake MFA rollout timeline" + client_types = ["SNOWFLAKE_UI", "SNOWFLAKE_CLI", "DRIVERS", "SNOWSQL"] + mfa_authentication_methods = ["PASSWORD"] + mfa_enrollment = "REQUIRED" +} + +import { + to = snowflake_authentication_policy.service_account_policy + id = "SERVICE_ACCOUNT_POLICY" +} + +resource "snowflake_authentication_policy" "service_account_policy" { + name = "SERVICE_ACCOUNT_POLICY" + comment = "Restricted client access policy for service accounts using key-pair authentication" + client_types = ["DRIVERS", "SNOWFLAKE_CLI", "SNOWSQL"] +} + +import { + to = snowflake_password_policy.admin_password_policy + id = "ADMIN_PASSWORD_POLICY" +} + +resource "snowflake_password_policy" "admin_password_policy" { + name = "ADMIN_PASSWORD_POLICY" + comment = "Enhanced password security for admin users" + password_history = 15 + password_lockout_time_mins = 60 + password_max_age_days = 60 + password_max_length = 256 + password_max_retries = 3 + password_min_length = 16 + password_min_lower_case_chars = 2 + password_min_numeric_chars = 2 + password_min_special_chars = 2 + password_min_upper_case_chars = 2 +} + +import { + to = snowflake_password_policy.service_password_policy + id = "SERVICE_PASSWORD_POLICY" +} + +resource "snowflake_password_policy" "service_password_policy" { + name = "SERVICE_PASSWORD_POLICY" + comment = "Temporary password policy for service accounts (migrate to RSA keys)" + password_history = 20 + password_lockout_time_mins = 120 + password_max_age_days = 180 + password_max_length = 256 + password_max_retries = 3 + password_min_length = 20 + password_min_lower_case_chars = 2 + password_min_numeric_chars = 3 + password_min_special_chars = 2 + password_min_upper_case_chars = 2 +} + +import { + to = snowflake_password_policy.standard_password_policy + id = "STANDARD_PASSWORD_POLICY" +} + +resource "snowflake_password_policy" "standard_password_policy" { + name = "STANDARD_PASSWORD_POLICY" + comment = "Standard password security for regular users" + password_history = 12 + password_lockout_time_mins = 30 + password_max_age_days = 90 + password_max_length = 256 + password_max_retries = 5 + password_min_length = 14 + password_min_lower_case_chars = 1 + password_min_numeric_chars = 2 + password_min_special_chars = 1 + password_min_upper_case_chars = 1 +} + +import { + to = snowflake_session_policy.admin_session_policy + id = "ADMIN_SESSION_POLICY" +} + +resource "snowflake_session_policy" "admin_session_policy" { + name = "ADMIN_SESSION_POLICY" + comment = "Strict session policy for admin users" + session_idle_timeout_mins = 15 + session_ui_idle_timeout_mins = 15 +} + +import { + to = snowflake_session_policy.analyst_session_policy + id = "ANALYST_SESSION_POLICY" +} + +resource "snowflake_session_policy" "analyst_session_policy" { + name = "ANALYST_SESSION_POLICY" + comment = "Extended session policy for data analysts running long queries" + session_idle_timeout_mins = 180 + session_ui_idle_timeout_mins = 240 +} + +import { + to = snowflake_session_policy.dev_session_policy + id = "DEV_SESSION_POLICY" +} + +resource "snowflake_session_policy" "dev_session_policy" { + name = "DEV_SESSION_POLICY" + comment = "Development environment session policy" + session_idle_timeout_mins = 120 + session_ui_idle_timeout_mins = 180 +} + +import { + to = snowflake_session_policy.service_session_policy + id = "SERVICE_SESSION_POLICY" +} + +resource "snowflake_session_policy" "service_session_policy" { + name = "SERVICE_SESSION_POLICY" + comment = "Session policy for service accounts and automation" + session_idle_timeout_mins = 240 + session_ui_idle_timeout_mins = 60 +} + +import { + to = snowflake_session_policy.user_session_policy + id = "USER_SESSION_POLICY" +} + +resource "snowflake_session_policy" "user_session_policy" { + name = "USER_SESSION_POLICY" + comment = "Standard session policy for regular users" + session_idle_timeout_mins = 60 + session_ui_idle_timeout_mins = 120 +} diff --git a/terraform-snowflake-boilerplate/resource_monitors.tf b/terraform-snowflake-boilerplate/resource_monitors.tf new file mode 100644 index 0000000..a213cf1 --- /dev/null +++ b/terraform-snowflake-boilerplate/resource_monitors.tf @@ -0,0 +1,107 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Resource monitor resources + +import { + to = snowflake_resource_monitor.analyst_warehouse_monitor + id = "ANALYST_WAREHOUSE_MONITOR" +} + +resource "snowflake_resource_monitor" "analyst_warehouse_monitor" { + name = "ANALYST_WAREHOUSE_MONITOR" + credit_quota = 100 + frequency = "MONTHLY" + notify_triggers = [75, 90] + suspend_triggers = [100] +} + +import { + to = snowflake_resource_monitor.dev_environment_monitor + id = "DEV_ENVIRONMENT_MONITOR" +} + +resource "snowflake_resource_monitor" "dev_environment_monitor" { + name = "DEV_ENVIRONMENT_MONITOR" + credit_quota = 500 + frequency = "MONTHLY" + notify_triggers = [60, 80] + suspend_triggers = [95] +} + +import { + to = snowflake_resource_monitor.dlt_pipeline_monitor + id = "DLT_PIPELINE_MONITOR" +} + +resource "snowflake_resource_monitor" "dlt_pipeline_monitor" { + name = "DLT_PIPELINE_MONITOR" + credit_quota = 300 + frequency = "MONTHLY" + notify_triggers = [80, 95] + suspend_triggers = [100] +} + +import { + to = snowflake_resource_monitor.emergency_cost_control + id = "EMERGENCY_COST_CONTROL" +} + +resource "snowflake_resource_monitor" "emergency_cost_control" { + name = "EMERGENCY_COST_CONTROL" + credit_quota = 1500 + frequency = "MONTHLY" + notify_triggers = [95] + suspend_triggers = [100] +} + +import { + to = snowflake_resource_monitor.pipeline_monitor + id = "PIPELINE_MONITOR" +} + +resource "snowflake_resource_monitor" "pipeline_monitor" { + name = "PIPELINE_MONITOR" + credit_quota = 500 + frequency = "MONTHLY" + notify_triggers = [70, 85] + suspend_triggers = [98] +} + +import { + to = snowflake_resource_monitor.snowtower_account_monitor + id = "SNOWTOWER_ACCOUNT_MONITOR" +} + +resource "snowflake_resource_monitor" "snowtower_account_monitor" { + name = "SNOWTOWER_ACCOUNT_MONITOR" + credit_quota = 1000 + frequency = "MONTHLY" + notify_triggers = [50, 75, 90] + suspend_triggers = [100] +} + +import { + to = snowflake_resource_monitor.snowtower_monitor + id = "SNOWTOWER_MONITOR" +} + +resource "snowflake_resource_monitor" "snowtower_monitor" { + name = "SNOWTOWER_MONITOR" + credit_quota = 50 + frequency = "MONTHLY" + notify_triggers = [75, 90] + suspend_triggers = [100] +} + +import { + to = snowflake_resource_monitor.weekly_spending_monitor + id = "WEEKLY_SPENDING_MONITOR" +} + +resource "snowflake_resource_monitor" "weekly_spending_monitor" { + name = "WEEKLY_SPENDING_MONITOR" + credit_quota = 150 + frequency = "WEEKLY" + notify_triggers = [80] +} diff --git a/terraform-snowflake-boilerplate/roles.tf b/terraform-snowflake-boilerplate/roles.tf new file mode 100644 index 0000000..b8efbc9 --- /dev/null +++ b/terraform-snowflake-boilerplate/roles.tf @@ -0,0 +1,374 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Role resources (business + tech) + +import { + to = snowflake_account_role.admin_role_b_role + id = "ADMIN_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "admin_role_b_role" { + name = "ADMIN_ROLE__B_ROLE" + comment = "Administrative business role with full access" +} + +import { + to = snowflake_account_role.ai_ml_role_b_role + id = "AI_ML_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "ai_ml_role_b_role" { + name = "AI_ML_ROLE__B_ROLE" + comment = "Business role for AI and ML workloads" +} + +import { + to = snowflake_account_role.bi_developer_role_b_role + id = "BI_DEVELOPER_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "bi_developer_role_b_role" { + name = "BI_DEVELOPER_ROLE__B_ROLE" + comment = "Business role for BI developers who create models and dashboards in ANALYTICS_TOOL using dbt transformations" +} + +import { + to = snowflake_account_role.company_users_b_role + id = "COMPANY_USERS__B_ROLE" +} + +resource "snowflake_account_role" "company_users_b_role" { + name = "COMPANY_USERS__B_ROLE" + comment = "Business role for SnowTower team members doing data work" +} + +import { + to = snowflake_account_role.data_integration_role_b_role + id = "DATA_INTEGRATION_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "data_integration_role_b_role" { + name = "DATA_INTEGRATION_ROLE__B_ROLE" + comment = "Business role for data integration services" +} + +import { + to = snowflake_account_role.dbt_analytics_role_b_role + id = "DBT_ANALYTICS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "dbt_analytics_role_b_role" { + name = "DBT_ANALYTICS_ROLE__B_ROLE" + comment = "Business role for dbt and analytics work - includes schema owner access for PROJ_STRIPE to modify existing tables/views" +} + +import { + to = snowflake_account_role.dlt_stripe_role_b_role + id = "DLT_STRIPE_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "dlt_stripe_role_b_role" { + name = "DLT_STRIPE_ROLE__B_ROLE" + comment = "Dedicated business role for DLT Stripe data pipeline - includes schema owner access for table stage operations" +} + +import { + to = snowflake_account_role.infrastructure_automation_role_b_role + id = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "infrastructure_automation_role_b_role" { + name = "INFRASTRUCTURE_AUTOMATION_ROLE__B_ROLE" + comment = "Business role for infrastructure automation and deployment services" +} + +import { + to = snowflake_account_role.lightdash_business_role_b_role + id = "LIGHTDASH_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "lightdash_business_role_b_role" { + name = "LIGHTDASH_BUSINESS_ROLE__B_ROLE" + comment = "Business role for LightDash BI platform - analytics and dashboard access with read/write to BI_TOOL database" +} + +import { + to = snowflake_account_role.omni_business_role_b_role + id = "OMNI_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "omni_business_role_b_role" { + name = "OMNI_BUSINESS_ROLE__B_ROLE" + comment = "Business role for Omni BI platform - analytics and reporting access" +} + +import { + to = snowflake_account_role.recce_business_role_b_role + id = "RECCE_BUSINESS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "recce_business_role_b_role" { + name = "RECCE_BUSINESS_ROLE__B_ROLE" + comment = "Business role for Recce dbt validation and data quality testing" +} + +import { + to = snowflake_account_role.service_accounts_role_b_role + id = "SERVICE_ACCOUNTS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "service_accounts_role_b_role" { + name = "SERVICE_ACCOUNTS_ROLE__B_ROLE" + comment = "Business role for service accounts and integrations" +} + +import { + to = snowflake_account_role.streamlit_apps_role_b_role + id = "STREAMLIT_APPS_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "streamlit_apps_role_b_role" { + name = "STREAMLIT_APPS_ROLE__B_ROLE" + comment = "Business role for Streamlit applications and services" +} + +import { + to = snowflake_account_role.training_role_b_role + id = "TRAINING_ROLE__B_ROLE" +} + +resource "snowflake_account_role" "training_role_b_role" { + name = "TRAINING_ROLE__B_ROLE" + comment = "Business role for training and development users" +} + +import { + to = snowflake_account_role.bi_writer_tech_role_t_role + id = "BI_WRITER_TECH_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "bi_writer_tech_role_t_role" { + name = "BI_WRITER_TECH_ROLE__T_ROLE" + comment = "Technical role for BI developers who need to read from transformed data and write to ANALYTICS_TOOL" +} + +import { + to = snowflake_account_role.company_users_t_role + id = "COMPANY_USERS__T_ROLE" +} + +resource "snowflake_account_role" "company_users_t_role" { + name = "COMPANY_USERS__T_ROLE" + comment = "SnowTower team members role with development database access" +} + +import { + to = snowflake_account_role.copilot_access_role_t_role + id = "COPILOT_ACCESS_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "copilot_access_role_t_role" { + name = "COPILOT_ACCESS_ROLE__T_ROLE" + comment = "Role for Copilot access and AI integrations" +} + +import { + to = snowflake_account_role.dbt_stripe_role_t_role + id = "DBT_STRIPE_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "dbt_stripe_role_t_role" { + name = "DBT_STRIPE_ROLE__T_ROLE" + comment = "dbt service role for Stripe transformations - Read-only access to SOURCE_STRIPE (DLT-owned source data), full read/write access to PROJ_STRIPE and ANALYTICS_TOOL for transformed models and analytics" +} + +import { + to = snowflake_account_role.dbt_training_t_role + id = "DBT_TRAINING__T_ROLE" +} + +resource "snowflake_account_role" "dbt_training_t_role" { + name = "DBT_TRAINING__T_ROLE" + comment = "Training role for dbt users and courses" +} + +import { + to = snowflake_account_role.dlt_loader_role_t_role + id = "DLT_LOADER_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "dlt_loader_role_t_role" { + name = "DLT_LOADER_ROLE__T_ROLE" + comment = "Data loading role for DLT operations with SOURCE_STRIPE permissions" +} + +import { + to = snowflake_account_role.dlt_stripe_tech_role_t_role + id = "DLT_STRIPE_TECH_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "dlt_stripe_tech_role_t_role" { + name = "DLT_STRIPE_TECH_ROLE__T_ROLE" + comment = "Dedicated technical role for DLT Stripe pipeline - comprehensive permissions for data loading operations including table stage access via schema owner role." +} + +import { + to = snowflake_account_role.estuary_t_role + id = "ESTUARY__T_ROLE" +} + +resource "snowflake_account_role" "estuary_t_role" { + name = "ESTUARY__T_ROLE" + comment = "Estuary data integration service role" +} + +import { + to = snowflake_account_role.fabi_ai_t_role + id = "FABI_AI__T_ROLE" +} + +resource "snowflake_account_role" "fabi_ai_t_role" { + name = "FABI_AI__T_ROLE" + comment = "Fabi AI service role for AI/ML operations" +} + +import { + to = snowflake_account_role.fivetran_t_role + id = "FIVETRAN__T_ROLE" +} + +resource "snowflake_account_role" "fivetran_t_role" { + name = "FIVETRAN__T_ROLE" + comment = "Fivetran data integration service role" +} + +import { + to = snowflake_account_role.fivetran_role_t_role + id = "FIVETRAN_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "fivetran_role_t_role" { + name = "FIVETRAN_ROLE__T_ROLE" + comment = "Alternative Fivetran role for data integration operations" +} + +import { + to = snowflake_account_role.lightdash_tech_role_t_role + id = "LIGHTDASH_TECH_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "lightdash_tech_role_t_role" { + name = "LIGHTDASH_TECH_ROLE__T_ROLE" + comment = "LightDash BI platform technical role - Read access to source data, full read/write access to BI_TOOL database for dashboard content and analytics" +} + +import { + to = snowflake_account_role.matillion_role_t_role + id = "MATILLION_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "matillion_role_t_role" { + name = "MATILLION_ROLE__T_ROLE" + comment = "Matillion ETL processing service role" +} + +import { + to = snowflake_account_role.omni_infrastructure_automation_t_role + id = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" +} + +resource "snowflake_account_role" "omni_infrastructure_automation_t_role" { + name = "OMNI_INFRASTRUCTURE_AUTOMATION__T_ROLE" + comment = "Omni infrastructure automation role for automated deployment and management" +} + +import { + to = snowflake_account_role.omni_tech_role_t_role + id = "OMNI_TECH_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "omni_tech_role_t_role" { + name = "OMNI_TECH_ROLE__T_ROLE" + comment = "Omni BI platform technical role - Read access to PROJ_STRIPE, full write access to ANALYTICS_TOOL database" +} + +import { + to = snowflake_account_role.recce_tech_role_t_role + id = "RECCE_TECH_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "recce_tech_role_t_role" { + name = "RECCE_TECH_ROLE__T_ROLE" + comment = "Technical role for Recce dbt validation - Read-only access to PROJ_STRIPE for model validation and data quality testing" +} + +import { + to = snowflake_account_role.sigma_role_t_role + id = "SIGMA_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "sigma_role_t_role" { + name = "SIGMA_ROLE__T_ROLE" + comment = "Sigma BI analytics service role" +} + +import { + to = snowflake_account_role.snowtower_users_t_role + id = "SNOWTOWER_USERS__T_ROLE" +} + +resource "snowflake_account_role" "snowtower_users_t_role" { + name = "SNOWTOWER_USERS__T_ROLE" + comment = "SnowTower application users role" +} + +import { + to = snowflake_account_role.streamlit_role_t_role + id = "STREAMLIT_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "streamlit_role_t_role" { + name = "STREAMLIT_ROLE__T_ROLE" + comment = "Streamlit applications service role" +} + +import { + to = snowflake_account_role.streamlit_towerapp_role_t_role + id = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" +} + +resource "snowflake_account_role" "streamlit_towerapp_role_t_role" { + name = "STREAMLIT_TOWERAPP_ROLE__T_ROLE" + comment = "SnowTower Streamlit application specific role" +} + +import { + to = snowflake_account_role.streamlit_viewer_t_role + id = "STREAMLIT_VIEWER__T_ROLE" +} + +resource "snowflake_account_role" "streamlit_viewer_t_role" { + name = "STREAMLIT_VIEWER__T_ROLE" + comment = "Read-only infrastructure viewer role for Streamlit app users - provides minimal metadata access without data privileges" +} + +import { + to = snowflake_account_role.stripe_t_role + id = "STRIPE__T_ROLE" +} + +resource "snowflake_account_role" "stripe_t_role" { + name = "STRIPE__T_ROLE" + comment = "Stripe data access role with read permissions to SOURCE_STRIPE and write permissions to PROJ_STRIPE" +} + +import { + to = snowflake_account_role.tobiko_cloud_t_role + id = "TOBIKO_CLOUD__T_ROLE" +} + +resource "snowflake_account_role" "tobiko_cloud_t_role" { + name = "TOBIKO_CLOUD__T_ROLE" + comment = "Tobiko Cloud service role" +} diff --git a/terraform-snowflake-boilerplate/users.tf b/terraform-snowflake-boilerplate/users.tf new file mode 100644 index 0000000..1f23372 --- /dev/null +++ b/terraform-snowflake-boilerplate/users.tf @@ -0,0 +1,64 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# User resources + +import { + to = snowflake_user.alice_admin + id = "ALICE_ADMIN" +} + +resource "snowflake_user" "alice_admin" { + name = "ALICE_ADMIN" + email = "alice.admin@example.com" + first_name = "Alice" + last_name = "Admin" + default_role = "SYSADMIN" + comment = "Platform administrator" + user_type = "PERSON" +} + +import { + to = snowflake_user.dbt_service + id = "DBT_SERVICE" +} + +resource "snowflake_user" "dbt_service" { + name = "DBT_SERVICE" + default_role = "DBT_ANALYTICS_ROLE__B_ROLE" + comment = "dbt transformation service account" + user_type = "SERVICE" + rsa_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0example... +...replace with your actual RSA public key... +AQAB" +} + +import { + to = snowflake_user.dlt_service + id = "DLT_SERVICE" +} + +resource "snowflake_user" "dlt_service" { + name = "DLT_SERVICE" + default_role = "DLT_STRIPE_ROLE__B_ROLE" + comment = "Data ingestion service account" + user_type = "SERVICE" + rsa_public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0example... +...replace with your actual RSA public key... +AQAB" +} + +import { + to = snowflake_user.john_analyst + id = "JOHN_ANALYST" +} + +resource "snowflake_user" "john_analyst" { + name = "JOHN_ANALYST" + email = "john.analyst@example.com" + first_name = "John" + last_name = "Analyst" + default_role = "COMPANY_USERS__B_ROLE" + comment = "Data analyst" + user_type = "PERSON" +} diff --git a/terraform-snowflake-boilerplate/warehouses.tf b/terraform-snowflake-boilerplate/warehouses.tf new file mode 100644 index 0000000..c8419d3 --- /dev/null +++ b/terraform-snowflake-boilerplate/warehouses.tf @@ -0,0 +1,231 @@ +# Generated by SnowTower - DO NOT EDIT MANUALLY +# Source: snowddl/ YAML configurations + +# Warehouse resources + +import { + to = snowflake_warehouse.admin + id = "ADMIN" +} + +resource "snowflake_warehouse" "admin" { + name = "ADMIN" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Administrative warehouse for admin operations" +} + +import { + to = snowflake_warehouse.analyst_wh + id = "ANALYST_WH" +} + +resource "snowflake_warehouse" "analyst_wh" { + name = "ANALYST_WH" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Warehouse for analyst workloads" + resource_monitor = "ANALYST_WAREHOUSE_MONITOR" +} + +import { + to = snowflake_warehouse.analytics_tool + id = "ANALYTICS_TOOL" +} + +resource "snowflake_warehouse" "analytics_tool" { + name = "ANALYTICS_TOOL" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Omni BI platform warehouse for analytics and reporting queries" +} + +import { + to = snowflake_warehouse.bi_tool + id = "BI_TOOL" +} + +resource "snowflake_warehouse" "bi_tool" { + name = "BI_TOOL" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "LightDash BI platform warehouse for analytics and dashboard queries" +} + +import { + to = snowflake_warehouse.dbt_stripe_wh + id = "DBT_STRIPE_WH" +} + +resource "snowflake_warehouse" "dbt_stripe_wh" { + name = "DBT_STRIPE_WH" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "dbt Stripe transformation operations" +} + +import { + to = snowflake_warehouse.dev_frank_wh + id = "DEV_FRANK_WH" +} + +resource "snowflake_warehouse" "dev_frank_wh" { + name = "DEV_FRANK_WH" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Polo's dedicated development warehouse for data engineering work" +} + +import { + to = snowflake_warehouse.dev_wh + id = "DEV_WH" +} + +resource "snowflake_warehouse" "dev_wh" { + name = "DEV_WH" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Development warehouse for personal dev environments" +} + +import { + to = snowflake_warehouse.dlt + id = "DLT" +} + +resource "snowflake_warehouse" "dlt" { + name = "DLT" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "DLT data loading and pipeline operations" + resource_monitor = "DLT_PIPELINE_MONITOR" +} + +import { + to = snowflake_warehouse.estuary + id = "ESTUARY" +} + +resource "snowflake_warehouse" "estuary" { + name = "ESTUARY" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Estuary data integration warehouse" +} + +import { + to = snowflake_warehouse.fabi_ai_warehouse + id = "FABI_AI_WAREHOUSE" +} + +resource "snowflake_warehouse" "fabi_ai_warehouse" { + name = "FABI_AI_WAREHOUSE" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Fabi AI workloads and model training" +} + +import { + to = snowflake_warehouse.fivetran + id = "FIVETRAN" +} + +resource "snowflake_warehouse" "fivetran" { + name = "FIVETRAN" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Fivetran data integration warehouse" +} + +import { + to = snowflake_warehouse.main_warehouse + id = "MAIN_WAREHOUSE" +} + +resource "snowflake_warehouse" "main_warehouse" { + name = "MAIN_WAREHOUSE" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "SnowTower team warehouse for data work and analytics" + resource_monitor = "DEV_ENVIRONMENT_MONITOR" +} + +import { + to = snowflake_warehouse.recce + id = "RECCE" +} + +resource "snowflake_warehouse" "recce" { + name = "RECCE" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Recce dbt validation and data quality testing warehouse" +} + +import { + to = snowflake_warehouse.streamlit_viewer_wh + id = "STREAMLIT_VIEWER_WH" +} + +resource "snowflake_warehouse" "streamlit_viewer_wh" { + name = "STREAMLIT_VIEWER_WH" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Dedicated warehouse for Streamlit viewer metadata queries - minimal resources for cost efficiency" + resource_monitor = "SNOWTOWER_MONITOR" + min_cluster_count = 1 + max_cluster_count = 1 +} + +import { + to = snowflake_warehouse.stripe + id = "STRIPE" +} + +resource "snowflake_warehouse" "stripe" { + name = "STRIPE" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Stripe data processing and analytics" +} + +import { + to = snowflake_warehouse.transforming + id = "TRANSFORMING" +} + +resource "snowflake_warehouse" "transforming" { + name = "TRANSFORMING" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "General data transformation warehouse" +} + +import { + to = snowflake_warehouse.transforming_user + id = "TRANSFORMING_USER" +} + +resource "snowflake_warehouse" "transforming_user" { + name = "TRANSFORMING_USER" + warehouse_size = "XSMALL" + auto_suspend = 60 + auto_resume = true + comment = "Heather's training transformation warehouse" +} diff --git a/tests/test_generate_terraform.py b/tests/test_generate_terraform.py new file mode 100644 index 0000000..b2f160a --- /dev/null +++ b/tests/test_generate_terraform.py @@ -0,0 +1,703 @@ +""" +Test Suite for Terraform HCL Generator from SnowDDL YAML. + +Tests the generate_terraform.py script including: +- Name conversion helpers (to_tf_name, hcl_value, hcl_block) +- YAML loading with !decrypt tag handling +- Individual generators (users, warehouses, roles, policies, etc.) +- End-to-end generate_all and write_to_directory +- Output rendering helpers +""" + +import sys +from pathlib import Path + +# Make scripts/ importable +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +import pytest +import yaml + +from generate_terraform import ( + to_tf_name, + hcl_value, + hcl_block, + load_yaml, + generate_users, + generate_warehouses, + generate_business_roles, + generate_tech_roles, + generate_network_policies, + generate_resource_monitors, + generate_databases, + generate_all, + write_to_directory, + render_section, + TerraformOutput, + HEADER, +) + + +# --------------------------------------------------------------------------- +# to_tf_name +# --------------------------------------------------------------------------- + + +class TestToTfName: + """Tests for the to_tf_name name conversion helper.""" + + def test_uppercase_to_lowercase(self): + assert to_tf_name("MY_WAREHOUSE") == "my_warehouse" + + def test_hyphens_to_underscores(self): + assert to_tf_name("my-warehouse") == "my_warehouse" + + def test_special_characters_replaced(self): + assert to_tf_name("WH@#$NAME") == "wh_name" + + def test_consecutive_underscores_collapsed(self): + assert to_tf_name("WH___NAME") == "wh_name" + + def test_leading_trailing_underscores_stripped(self): + assert to_tf_name("__WH__") == "wh" + + def test_dots_replaced(self): + assert to_tf_name("DB.SCHEMA") == "db_schema" + + def test_mixed_case_and_special(self): + assert to_tf_name("My-Cool_Role__B_ROLE") == "my_cool_role_b_role" + + def test_already_valid(self): + assert to_tf_name("simple_name") == "simple_name" + + def test_single_char(self): + assert to_tf_name("X") == "x" + + def test_numbers_preserved(self): + assert to_tf_name("WH_2X_LARGE") == "wh_2x_large" + + +# --------------------------------------------------------------------------- +# hcl_value +# --------------------------------------------------------------------------- + + +class TestHclValue: + """Tests for the hcl_value formatting function.""" + + def test_bool_true(self): + assert hcl_value(True) == "true" + + def test_bool_false(self): + assert hcl_value(False) == "false" + + def test_int(self): + assert hcl_value(42) == "42" + + def test_float(self): + assert hcl_value(3.14) == "3.14" + + def test_string(self): + assert hcl_value("hello") == '"hello"' + + def test_string_with_quotes(self): + assert hcl_value('say "hi"') == '"say \\"hi\\""' + + def test_string_with_backslash(self): + assert hcl_value("back\\slash") == '"back\\\\slash"' + + def test_list_of_strings(self): + assert hcl_value(["a", "b"]) == '["a", "b"]' + + def test_list_of_ints(self): + assert hcl_value([1, 2, 3]) == "[1, 2, 3]" + + def test_empty_list(self): + assert hcl_value([]) == "[]" + + def test_fallback_type(self): + """Non-standard types are stringified.""" + result = hcl_value(None) + assert result == '"None"' + + +# --------------------------------------------------------------------------- +# hcl_block +# --------------------------------------------------------------------------- + + +class TestHclBlock: + """Tests for the hcl_block rendering function.""" + + def test_basic_block(self): + result = hcl_block("snowflake_user", "alice", {"name": "ALICE"}) + assert 'resource "snowflake_user" "alice"' in result + assert 'name = "ALICE"' in result + assert result.endswith("}") + + def test_block_with_import_id(self): + result = hcl_block("snowflake_user", "alice", {"name": "ALICE"}, import_id="ALICE") + assert "import {" in result + assert "to = snowflake_user.alice" in result + assert 'id = "ALICE"' in result + + def test_block_without_import_id(self): + result = hcl_block("snowflake_user", "alice", {"name": "ALICE"}) + assert "import" not in result + + def test_nested_block(self): + """Nested dict values render as HCL sub-blocks.""" + attrs = { + "name": "WH", + "on_account_object": {"object_type": "WAREHOUSE", "object_name": "WH"}, + } + result = hcl_block("snowflake_grant", "g", attrs) + assert "on_account_object {" in result + assert "object_type" in result + + def test_bool_and_int_values(self): + attrs = {"auto_resume": True, "auto_suspend": 120} + result = hcl_block("snowflake_warehouse", "wh", attrs) + assert "true" in result + assert "120" in result + + def test_empty_attrs(self): + """A block with no attributes still renders valid HCL.""" + result = hcl_block("snowflake_user", "empty", {}) + assert 'resource "snowflake_user" "empty" {' in result + assert result.strip().endswith("}") + + +# --------------------------------------------------------------------------- +# load_yaml +# --------------------------------------------------------------------------- + + +class TestLoadYaml: + """Tests for the load_yaml helper.""" + + def test_valid_file(self, tmp_path): + p = tmp_path / "test.yaml" + p.write_text(yaml.dump({"KEY": "value"})) + data = load_yaml(p) + assert data == {"KEY": "value"} + + def test_missing_file(self, tmp_path): + data = load_yaml(tmp_path / "missing.yaml") + assert data is None + + def test_empty_file(self, tmp_path): + p = tmp_path / "empty.yaml" + p.write_text("") + data = load_yaml(p) + assert data is None # None because safe_load returns None, not dict + + def test_non_dict_file(self, tmp_path): + p = tmp_path / "list.yaml" + p.write_text("- item1\n- item2\n") + data = load_yaml(p) + assert data is None # list is not dict + + def test_decrypt_tag_handled(self, tmp_path): + """The !decrypt tag should not raise an error and returns a placeholder.""" + p = tmp_path / "encrypted.yaml" + p.write_text('USER:\n password: !decrypt "gAAAAABf..."\n') + data = load_yaml(p) + assert data is not None + assert data["USER"]["password"] == "" + + +# --------------------------------------------------------------------------- +# generate_users +# --------------------------------------------------------------------------- + + +class TestGenerateUsers: + """Tests for generate_users.""" + + def test_generates_user_blocks(self, tmp_path): + data = { + "ALICE": { + "type": "PERSON", + "email": "alice@example.com", + "first_name": "Alice", + "last_name": "Smith", + "default_role": "ANALYST", + "comment": "Test user", + } + } + (tmp_path / "user.yaml").write_text(yaml.dump(data)) + blocks = generate_users(tmp_path) + assert len(blocks) == 1 + assert "snowflake_user" in blocks[0] + assert '"ALICE"' in blocks[0] + assert '"alice@example.com"' in blocks[0] + assert "user_type" in blocks[0] # 'type' maps to 'user_type' + + def test_rsa_key_included(self, tmp_path): + data = { + "SVC": { + "type": "SERVICE", + "rsa_public_key": "MIIBIjANBg...", + } + } + (tmp_path / "user.yaml").write_text(yaml.dump(data)) + blocks = generate_users(tmp_path) + assert len(blocks) == 1 + assert "rsa_public_key" in blocks[0] + + def test_empty_user_yaml(self, tmp_path): + (tmp_path / "user.yaml").write_text("") + blocks = generate_users(tmp_path) + assert blocks == [] + + def test_no_user_yaml(self, tmp_path): + blocks = generate_users(tmp_path) + assert blocks == [] + + def test_multiple_users_sorted(self, tmp_path): + data = { + "ZARA": {"type": "PERSON"}, + "ALICE": {"type": "PERSON"}, + } + (tmp_path / "user.yaml").write_text(yaml.dump(data)) + blocks = generate_users(tmp_path) + assert len(blocks) == 2 + # ALICE should come first (sorted) + assert "ALICE" in blocks[0] + assert "ZARA" in blocks[1] + + def test_import_block_present(self, tmp_path): + data = {"BOB": {"type": "PERSON"}} + (tmp_path / "user.yaml").write_text(yaml.dump(data)) + blocks = generate_users(tmp_path) + assert "import {" in blocks[0] + assert 'id = "BOB"' in blocks[0] + + +# --------------------------------------------------------------------------- +# generate_warehouses +# --------------------------------------------------------------------------- + + +class TestGenerateWarehouses: + """Tests for generate_warehouses.""" + + def test_generates_warehouse_block(self, tmp_path): + data = { + "COMPUTE_WH": { + "size": "X-Small", + "auto_suspend": 120, + "auto_resume": True, + "comment": "Main warehouse", + } + } + (tmp_path / "warehouse.yaml").write_text(yaml.dump(data)) + blocks = generate_warehouses(tmp_path) + assert len(blocks) == 1 + assert "snowflake_warehouse" in blocks[0] + assert '"COMPUTE_WH"' in blocks[0] + assert "XSMALL" in blocks[0] # size mapped + assert "120" in blocks[0] + + def test_size_mapping(self, tmp_path): + """Various size formats are correctly mapped.""" + for yaml_size, expected_tf in [("Small", "SMALL"), ("X-Large", "XLARGE"), ("2X-Large", "XXLARGE")]: + data = {"WH": {"size": yaml_size}} + (tmp_path / "warehouse.yaml").write_text(yaml.dump(data)) + blocks = generate_warehouses(tmp_path) + assert expected_tf in blocks[0], f"Expected {expected_tf} for size {yaml_size}" + + def test_default_auto_resume(self, tmp_path): + """auto_resume defaults to True if not specified.""" + data = {"WH": {"size": "Small"}} + (tmp_path / "warehouse.yaml").write_text(yaml.dump(data)) + blocks = generate_warehouses(tmp_path) + assert "auto_resume" in blocks[0] + assert "true" in blocks[0] + + def test_empty_warehouse_yaml(self, tmp_path): + (tmp_path / "warehouse.yaml").write_text("") + blocks = generate_warehouses(tmp_path) + assert blocks == [] + + def test_resource_monitor_included(self, tmp_path): + data = {"WH": {"size": "Small", "resource_monitor": "MY_MON"}} + (tmp_path / "warehouse.yaml").write_text(yaml.dump(data)) + blocks = generate_warehouses(tmp_path) + assert "resource_monitor" in blocks[0] + assert "MY_MON" in blocks[0] + + +# --------------------------------------------------------------------------- +# generate_business_roles +# --------------------------------------------------------------------------- + + +class TestGenerateBusinessRoles: + """Tests for generate_business_roles.""" + + def test_generates_role_and_grant_blocks(self, tmp_path): + data = { + "ANALYST": { + "tech_roles": ["READ_ROLE"], + "warehouse_usage": ["COMPUTE_WH"], + "comment": "Analyst role", + } + } + (tmp_path / "business_role.yaml").write_text(yaml.dump(data)) + role_blocks, grant_blocks = generate_business_roles(tmp_path) + assert len(role_blocks) == 1 + assert "ANALYST__B_ROLE" in role_blocks[0] + # tech role inheritance grant + warehouse usage grant + assert len(grant_blocks) == 2 + + def test_schema_owner_grant(self, tmp_path): + data = { + "ANALYST": { + "schema_owner": ["MY_DB.MY_SCHEMA"], + } + } + (tmp_path / "business_role.yaml").write_text(yaml.dump(data)) + role_blocks, grant_blocks = generate_business_roles(tmp_path) + assert len(grant_blocks) == 1 + assert "OWNER__S_ROLE" in grant_blocks[0] + + def test_empty_business_role_yaml(self, tmp_path): + (tmp_path / "business_role.yaml").write_text("") + role_blocks, grant_blocks = generate_business_roles(tmp_path) + assert role_blocks == [] + assert grant_blocks == [] + + def test_no_file(self, tmp_path): + role_blocks, grant_blocks = generate_business_roles(tmp_path) + assert role_blocks == [] + assert grant_blocks == [] + + def test_role_import_block(self, tmp_path): + data = {"ANALYST": {"comment": "test"}} + (tmp_path / "business_role.yaml").write_text(yaml.dump(data)) + role_blocks, _ = generate_business_roles(tmp_path) + assert "import {" in role_blocks[0] + assert "ANALYST__B_ROLE" in role_blocks[0] + + +# --------------------------------------------------------------------------- +# generate_tech_roles +# --------------------------------------------------------------------------- + + +class TestGenerateTechRoles: + """Tests for generate_tech_roles.""" + + def test_generates_role_and_grants(self, tmp_path): + data = { + "READ_ROLE": { + "grants": { + "DATABASE:USAGE": ["MY_DB"], + "SCHEMA:USAGE": ["MY_DB.PUBLIC"], + }, + } + } + (tmp_path / "tech_role.yaml").write_text(yaml.dump(data)) + role_blocks, grant_blocks = generate_tech_roles(tmp_path) + assert len(role_blocks) == 1 + assert "READ_ROLE__T_ROLE" in role_blocks[0] + assert len(grant_blocks) == 2 # one for DATABASE, one for SCHEMA + + def test_future_grants(self, tmp_path): + data = { + "READ_ROLE": { + "future_grants": { + "TABLE:SELECT": ["MY_DB"], + }, + } + } + (tmp_path / "tech_role.yaml").write_text(yaml.dump(data)) + role_blocks, grant_blocks = generate_tech_roles(tmp_path) + assert len(grant_blocks) == 1 + assert "future" in grant_blocks[0].lower() or "TABLES" in grant_blocks[0] + + def test_schema_grant_has_on_schema(self, tmp_path): + data = { + "ROLE": { + "grants": {"SCHEMA:USAGE": ["DB.SCHEMA"]}, + } + } + (tmp_path / "tech_role.yaml").write_text(yaml.dump(data)) + _, grant_blocks = generate_tech_roles(tmp_path) + assert "on_schema" in grant_blocks[0] + + def test_warehouse_grant_has_on_account_object(self, tmp_path): + data = { + "ROLE": { + "grants": {"WAREHOUSE:USAGE": ["WH"]}, + } + } + (tmp_path / "tech_role.yaml").write_text(yaml.dump(data)) + _, grant_blocks = generate_tech_roles(tmp_path) + assert "on_account_object" in grant_blocks[0] + + def test_empty_tech_role_yaml(self, tmp_path): + (tmp_path / "tech_role.yaml").write_text("") + role_blocks, grant_blocks = generate_tech_roles(tmp_path) + assert role_blocks == [] + assert grant_blocks == [] + + def test_empty_targets_skipped(self, tmp_path): + data = { + "ROLE": { + "grants": {"DATABASE:USAGE": []}, + } + } + (tmp_path / "tech_role.yaml").write_text(yaml.dump(data)) + _, grant_blocks = generate_tech_roles(tmp_path) + assert grant_blocks == [] + + +# --------------------------------------------------------------------------- +# generate_network_policies +# --------------------------------------------------------------------------- + + +class TestGenerateNetworkPolicies: + """Tests for generate_network_policies.""" + + def test_generates_policy_block(self, tmp_path): + data = { + "office_policy": { + "allowed_ip_list": ["10.0.0.0/8"], + "comment": "Office access", + } + } + (tmp_path / "network_policy.yaml").write_text(yaml.dump(data)) + blocks = generate_network_policies(tmp_path) + assert len(blocks) == 1 + assert "snowflake_network_policy" in blocks[0] + assert "OFFICE_POLICY" in blocks[0] # uppercased + assert "10.0.0.0/8" in blocks[0] + + def test_empty_returns_empty(self, tmp_path): + (tmp_path / "network_policy.yaml").write_text("") + assert generate_network_policies(tmp_path) == [] + + def test_no_file_returns_empty(self, tmp_path): + assert generate_network_policies(tmp_path) == [] + + +# --------------------------------------------------------------------------- +# generate_resource_monitors +# --------------------------------------------------------------------------- + + +class TestGenerateResourceMonitors: + """Tests for generate_resource_monitors.""" + + def test_generates_monitor_block(self, tmp_path): + data = { + "DAILY_MON": { + "credit_quota": 100, + "frequency": "DAILY", + "triggers": { + 80: "NOTIFY", + 95: "SUSPEND", + 100: "SUSPEND_IMMEDIATE", + }, + } + } + (tmp_path / "resource_monitor.yaml").write_text(yaml.dump(data)) + blocks = generate_resource_monitors(tmp_path) + assert len(blocks) == 1 + assert "snowflake_resource_monitor" in blocks[0] + assert "100" in blocks[0] # credit_quota + assert "notify_triggers" in blocks[0] + assert "suspend_triggers" in blocks[0] + assert "suspend_immediate_triggers" in blocks[0] + + def test_no_triggers(self, tmp_path): + data = {"MON": {"credit_quota": 50}} + (tmp_path / "resource_monitor.yaml").write_text(yaml.dump(data)) + blocks = generate_resource_monitors(tmp_path) + assert len(blocks) == 1 + assert "notify_triggers" not in blocks[0] + + def test_empty_returns_empty(self, tmp_path): + (tmp_path / "resource_monitor.yaml").write_text("") + assert generate_resource_monitors(tmp_path) == [] + + +# --------------------------------------------------------------------------- +# generate_databases +# --------------------------------------------------------------------------- + + +class TestGenerateDatabases: + """Tests for generate_databases.""" + + def test_generates_database_block(self, tmp_path): + db_dir = tmp_path / "MY_DB" + db_dir.mkdir() + (db_dir / "params.yaml").write_text(yaml.dump({"comment": "Production DB"})) + blocks = generate_databases(tmp_path) + assert len(blocks) == 1 + assert "snowflake_database" in blocks[0] + assert '"MY_DB"' in blocks[0] + assert "Production DB" in blocks[0] + + def test_sandbox_database(self, tmp_path): + db_dir = tmp_path / "SANDBOX_DB" + db_dir.mkdir() + (db_dir / "params.yaml").write_text(yaml.dump({"is_sandbox": True})) + blocks = generate_databases(tmp_path) + assert len(blocks) == 1 + assert "Sandbox" in blocks[0] or "sandbox" in blocks[0].lower() + + def test_no_databases(self, tmp_path): + blocks = generate_databases(tmp_path) + assert blocks == [] + + def test_multiple_databases_sorted(self, tmp_path): + for name in ["ZOO_DB", "ALPHA_DB"]: + d = tmp_path / name + d.mkdir() + (d / "params.yaml").write_text(yaml.dump({"comment": name})) + blocks = generate_databases(tmp_path) + assert len(blocks) == 2 + assert "ALPHA_DB" in blocks[0] + assert "ZOO_DB" in blocks[1] + + +# --------------------------------------------------------------------------- +# render_section +# --------------------------------------------------------------------------- + + +class TestRenderSection: + """Tests for the render_section helper.""" + + def test_non_empty_blocks(self): + result = render_section("Users", ['resource "snowflake_user" "a" {\n}']) + assert "# Users" in result + assert HEADER in result + assert "snowflake_user" in result + + def test_empty_blocks(self): + """Empty block list returns empty string.""" + result = render_section("Users", []) + assert result == "" + + def test_multiple_blocks_joined(self): + blocks = ["block_a", "block_b"] + result = render_section("Section", blocks) + assert "block_a" in result + assert "block_b" in result + + +# --------------------------------------------------------------------------- +# generate_all (end-to-end) +# --------------------------------------------------------------------------- + + +class TestGenerateAll: + """End-to-end tests for generate_all.""" + + def _write_yaml(self, path, data): + path.write_text(yaml.dump(data, default_flow_style=False)) + + def test_full_generation(self, tmp_path): + """generate_all with a complete config set produces all sections.""" + self._write_yaml( + tmp_path / "user.yaml", + {"ALICE": {"type": "PERSON", "email": "a@b.com"}}, + ) + self._write_yaml( + tmp_path / "warehouse.yaml", + {"WH": {"size": "Small"}}, + ) + self._write_yaml( + tmp_path / "business_role.yaml", + {"ANALYST": {"tech_roles": ["READ_ROLE"]}}, + ) + self._write_yaml( + tmp_path / "tech_role.yaml", + {"READ_ROLE": {"grants": {"DATABASE:USAGE": ["DB"]}}}, + ) + self._write_yaml( + tmp_path / "network_policy.yaml", + {"POL": {"allowed_ip_list": ["10.0.0.0/8"]}}, + ) + self._write_yaml( + tmp_path / "resource_monitor.yaml", + {"MON": {"credit_quota": 50}}, + ) + + output = generate_all(tmp_path) + assert isinstance(output, TerraformOutput) + assert len(output.users) == 1 + assert len(output.warehouses) == 1 + assert len(output.roles) == 2 # 1 business + 1 tech + assert len(output.grants) >= 1 + assert len(output.policies) >= 1 + assert len(output.resource_monitors) == 1 + assert "terraform" in output.main.lower() + + def test_empty_directory(self, tmp_path): + """generate_all with an empty directory returns empty lists.""" + output = generate_all(tmp_path) + assert output.users == [] + assert output.warehouses == [] + assert output.roles == [] + assert output.grants == [] + assert output.resource_monitors == [] + assert output.databases == [] + + +# --------------------------------------------------------------------------- +# write_to_directory +# --------------------------------------------------------------------------- + + +class TestWriteToDirectory: + """Tests for write_to_directory.""" + + def test_creates_files(self, tmp_path): + """write_to_directory creates .tf files in the output directory.""" + output = TerraformOutput() + output.main = "terraform {}" + output.users = ['resource "snowflake_user" "alice" {\n name = "ALICE"\n}'] + output.warehouses = ['resource "snowflake_warehouse" "wh" {\n name = "WH"\n}'] + + out_dir = tmp_path / "terraform_out" + write_to_directory(output, out_dir) + + assert out_dir.is_dir() + assert (out_dir / "main.tf").exists() + assert (out_dir / "users.tf").exists() + assert (out_dir / "warehouses.tf").exists() + + # main.tf should contain the provider block + main_content = (out_dir / "main.tf").read_text() + assert "terraform" in main_content + + def test_skips_empty_sections(self, tmp_path): + """Empty sections do not create files.""" + output = TerraformOutput() + output.main = "terraform {}" + # Everything else empty + + out_dir = tmp_path / "terraform_out" + write_to_directory(output, out_dir) + + assert (out_dir / "main.tf").exists() + assert not (out_dir / "users.tf").exists() + assert not (out_dir / "warehouses.tf").exists() + + def test_creates_parent_directories(self, tmp_path): + """write_to_directory creates parent directories if needed.""" + output = TerraformOutput() + output.main = "terraform {}" + + out_dir = tmp_path / "deep" / "nested" / "dir" + write_to_directory(output, out_dir) + assert out_dir.is_dir() + assert (out_dir / "main.tf").exists() diff --git a/tests/test_validate_config.py b/tests/test_validate_config.py new file mode 100644 index 0000000..5121032 --- /dev/null +++ b/tests/test_validate_config.py @@ -0,0 +1,947 @@ +""" +Test Suite for SnowDDL YAML Configuration Validator. + +Tests the validate_config.py script including: +- ValidationResult dataclass +- YAML loading helpers +- Per-file validators (user, business_role, tech_role, warehouse, network_policy, resource_monitor) +- Cross-reference integrity checks +- End-to-end run_validation orchestrator +""" + +import sys +from pathlib import Path + +# Make scripts/ importable +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +import pytest +import yaml + +from validate_config import ( + ValidationResult, + _load_yaml, + validate_user_yaml, + validate_business_role_yaml, + validate_tech_role_yaml, + validate_warehouse_yaml, + validate_network_policy_yaml, + validate_resource_monitor_yaml, + run_validation, + _cross_reference_checks, +) + + +# --------------------------------------------------------------------------- +# ValidationResult dataclass +# --------------------------------------------------------------------------- + + +class TestValidationResult: + """Tests for the ValidationResult dataclass.""" + + def test_initial_state(self): + """A fresh result has empty lists and no errors/warnings.""" + result = ValidationResult() + assert result.errors == [] + assert result.warnings == [] + assert result.info == [] + assert result.has_errors is False + assert result.has_warnings is False + + def test_error_method(self): + """error() appends to the errors list.""" + result = ValidationResult() + result.error("something broke") + assert result.errors == ["something broke"] + assert result.has_errors is True + assert result.has_warnings is False + + def test_warning_method(self): + """warning() appends to the warnings list.""" + result = ValidationResult() + result.warning("heads up") + assert result.warnings == ["heads up"] + assert result.has_warnings is True + assert result.has_errors is False + + def test_ok_method(self): + """ok() appends to the info list.""" + result = ValidationResult() + result.ok("all good") + assert result.info == ["all good"] + assert result.has_errors is False + assert result.has_warnings is False + + def test_multiple_messages(self): + """Multiple messages accumulate correctly.""" + result = ValidationResult() + result.error("err1") + result.error("err2") + result.warning("warn1") + result.ok("info1") + assert len(result.errors) == 2 + assert len(result.warnings) == 1 + assert len(result.info) == 1 + + +# --------------------------------------------------------------------------- +# _load_yaml helper +# --------------------------------------------------------------------------- + + +class TestLoadYaml: + """Tests for the _load_yaml helper function.""" + + def test_valid_yaml(self, tmp_path): + """Loading a valid YAML mapping returns (data, None).""" + p = tmp_path / "good.yaml" + p.write_text(yaml.dump({"KEY": {"type": "PERSON"}})) + data, err = _load_yaml(p) + assert err is None + assert data == {"KEY": {"type": "PERSON"}} + + def test_invalid_yaml_syntax(self, tmp_path): + """Loading broken YAML returns (None, error_message).""" + p = tmp_path / "bad.yaml" + p.write_text("key: [unterminated") + data, err = _load_yaml(p) + assert data is None + assert "YAML syntax error" in err + + def test_missing_file(self, tmp_path): + """Loading a non-existent file returns (None, error_message).""" + p = tmp_path / "nonexistent.yaml" + data, err = _load_yaml(p) + assert data is None + assert "File not found" in err + + def test_non_dict_yaml(self, tmp_path): + """Loading a YAML file whose root is a list returns (None, error).""" + p = tmp_path / "list.yaml" + p.write_text("- item1\n- item2\n") + data, err = _load_yaml(p) + assert data is None + assert "Expected a YAML mapping" in err + + def test_empty_yaml(self, tmp_path): + """Loading an empty YAML file returns ({}, None).""" + p = tmp_path / "empty.yaml" + p.write_text("") + data, err = _load_yaml(p) + assert err is None + assert data == {} + + +# --------------------------------------------------------------------------- +# validate_user_yaml +# --------------------------------------------------------------------------- + + +class TestValidateUserYaml: + """Tests for validate_user_yaml.""" + + def test_valid_person_user(self): + """A correctly defined PERSON user produces no errors.""" + data = { + "ALICE": { + "type": "PERSON", + "email": "alice@example.com", + "default_role": "SYSADMIN", + } + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert not result.has_errors + assert not result.has_warnings + + def test_valid_service_user(self): + """A correctly defined SERVICE user with RSA key produces no errors.""" + data = { + "SVC_PIPELINE": { + "type": "SERVICE", + "rsa_public_key": "MIIBIjANBgkqhkiG9w0BAQE...", + } + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert not result.has_errors + assert not result.has_warnings + + def test_missing_type_field(self): + """A user without the 'type' field produces an error.""" + data = {"BOB": {"email": "bob@example.com"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_errors + assert any("missing required 'type'" in e for e in result.errors) + + def test_invalid_type_field(self): + """A user with an invalid 'type' value produces an error.""" + data = {"CARL": {"type": "ROBOT"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_errors + assert any("invalid type 'ROBOT'" in e for e in result.errors) + + def test_person_without_email(self): + """A PERSON user without email produces a warning.""" + data = {"DIANE": {"type": "PERSON"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert not result.has_errors + assert result.has_warnings + assert any("should have 'email'" in w for w in result.warnings) + + def test_service_without_rsa_key(self): + """A SERVICE user without rsa_public_key produces a warning.""" + data = {"SVC_NO_KEY": {"type": "SERVICE"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_warnings + assert any("should have 'rsa_public_key'" in w for w in result.warnings) + + def test_service_with_placeholder_rsa_key(self): + """A SERVICE user with a placeholder RSA key produces a warning.""" + data = { + "SVC_PLACEHOLDER": { + "type": "SERVICE", + "rsa_public_key": "replace-this-with-real-key", + } + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_warnings + assert any("placeholder" in w for w in result.warnings) + + def test_service_with_example_rsa_key(self): + """A SERVICE user with 'example' in RSA key produces a warning.""" + data = { + "SVC_EXAMPLE": { + "type": "SERVICE", + "rsa_public_key": "example-public-key-data", + } + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_warnings + assert any("placeholder" in w for w in result.warnings) + + def test_duplicate_users(self): + """Duplicate user names (case-insensitive) produce an error.""" + data = { + "admin_user": {"type": "PERSON", "email": "a@b.com"}, + "ADMIN_USER": {"type": "PERSON", "email": "a@b.com"}, + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_errors + assert any("duplicate user name" in e for e in result.errors) + + def test_default_role_cross_reference_valid_system_role(self): + """A default_role pointing at a system role produces no error.""" + data = {"EVE": {"type": "PERSON", "email": "e@b.com", "default_role": "SYSADMIN"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles={"ANALYST"}) + assert not result.has_errors + + def test_default_role_cross_reference_valid_business_role(self): + """A default_role matching a defined business role produces no error.""" + data = {"FRANK": {"type": "PERSON", "email": "f@b.com", "default_role": "ANALYST"}} + result = ValidationResult() + validate_user_yaml(data, result, business_roles={"ANALYST"}) + assert not result.has_errors + + def test_default_role_cross_reference_with_suffix(self): + """A default_role using __B_ROLE suffix resolves correctly.""" + data = { + "GINA": { + "type": "PERSON", + "email": "g@b.com", + "default_role": "ANALYST__B_ROLE", + } + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles={"ANALYST"}) + assert not result.has_errors + + def test_default_role_cross_reference_invalid(self): + """A default_role not matching any role produces an error.""" + data = { + "HANK": {"type": "PERSON", "email": "h@b.com", "default_role": "NONEXISTENT"} + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles={"ANALYST"}) + assert result.has_errors + assert any("does not match" in e for e in result.errors) + + def test_non_dict_user_entry(self): + """A user whose config is not a dict produces an error.""" + data = {"BAD_USER": "just a string"} + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert result.has_errors + assert any("expected a mapping" in e for e in result.errors) + + def test_info_message_count(self): + """After validation, an info message reports the user count.""" + data = { + "U1": {"type": "PERSON", "email": "u1@b.com"}, + "U2": {"type": "SERVICE", "rsa_public_key": "MIIBIj..."}, + } + result = ValidationResult() + validate_user_yaml(data, result, business_roles=set()) + assert any("2 users validated" in i for i in result.info) + + +# --------------------------------------------------------------------------- +# validate_business_role_yaml +# --------------------------------------------------------------------------- + + +class TestValidateBusinessRoleYaml: + """Tests for validate_business_role_yaml.""" + + def test_valid_role(self): + """A valid business role with known tech_roles and warehouses passes.""" + data = { + "ANALYST": { + "tech_roles": ["READ_ROLE"], + "warehouse_usage": ["COMPUTE_WH"], + "schema_owner": ["MY_DB.MY_SCHEMA"], + } + } + result = ValidationResult() + validate_business_role_yaml( + data, result, tech_roles={"READ_ROLE"}, warehouses={"COMPUTE_WH"} + ) + assert not result.has_errors + + def test_missing_tech_role_reference(self): + """Referencing a nonexistent tech_role produces an error.""" + data = {"ANALYST": {"tech_roles": ["GHOST_ROLE"]}} + result = ValidationResult() + validate_business_role_yaml( + data, result, tech_roles={"READ_ROLE"}, warehouses=set() + ) + assert result.has_errors + assert any("GHOST_ROLE" in e for e in result.errors) + + def test_missing_warehouse_reference(self): + """Referencing a nonexistent warehouse produces an error.""" + data = {"ANALYST": {"warehouse_usage": ["GHOST_WH"]}} + result = ValidationResult() + validate_business_role_yaml( + data, result, tech_roles=set(), warehouses={"COMPUTE_WH"} + ) + assert result.has_errors + assert any("GHOST_WH" in e for e in result.errors) + + def test_invalid_schema_owner_format(self): + """A schema_owner not in DB.SCHEMA format produces an error.""" + data = {"ANALYST": {"schema_owner": ["JUST_A_DB"]}} + result = ValidationResult() + validate_business_role_yaml(data, result, tech_roles=set(), warehouses=set()) + assert result.has_errors + assert any("DB.SCHEMA format" in e for e in result.errors) + + def test_valid_schema_owner_format(self): + """A schema_owner in proper DB.SCHEMA format passes.""" + data = {"ANALYST": {"schema_owner": ["PROD_DB.ANALYTICS"]}} + result = ValidationResult() + validate_business_role_yaml(data, result, tech_roles=set(), warehouses=set()) + assert not result.has_errors + + def test_schema_owner_with_special_chars(self): + """A schema_owner with hyphens is invalid (only underscores allowed).""" + data = {"ANALYST": {"schema_owner": ["PROD-DB.ANALYTICS"]}} + result = ValidationResult() + validate_business_role_yaml(data, result, tech_roles=set(), warehouses=set()) + assert result.has_errors + + def test_non_dict_role_entry(self): + """A role whose config is not a dict produces an error.""" + data = {"BAD_ROLE": "not a dict"} + result = ValidationResult() + validate_business_role_yaml(data, result, tech_roles=set(), warehouses=set()) + assert result.has_errors + assert any("expected a mapping" in e for e in result.errors) + + def test_duplicate_role_names(self): + """Duplicate business role names produce an error.""" + data = { + "analyst": {"tech_roles": []}, + "ANALYST": {"tech_roles": []}, + } + result = ValidationResult() + validate_business_role_yaml(data, result, tech_roles=set(), warehouses=set()) + assert result.has_errors + assert any("duplicate role name" in e for e in result.errors) + + def test_empty_tech_roles_no_cross_ref(self): + """When tech_roles set is empty, no cross-reference errors are raised.""" + data = {"ANALYST": {"tech_roles": ["ANY_ROLE"]}} + result = ValidationResult() + validate_business_role_yaml( + data, result, tech_roles=set(), warehouses=set() + ) + assert not result.has_errors + + +# --------------------------------------------------------------------------- +# validate_tech_role_yaml +# --------------------------------------------------------------------------- + + +class TestValidateTechRoleYaml: + """Tests for validate_tech_role_yaml.""" + + def test_valid_grant_keys(self): + """Properly formatted grant keys pass validation.""" + data = { + "READ_ROLE": { + "grants": { + "DATABASE:USAGE": ["MY_DB"], + "SCHEMA:USAGE": ["MY_DB.PUBLIC"], + }, + } + } + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert not result.has_errors + + def test_invalid_grant_key_no_colon(self): + """A grant key without a colon produces an error.""" + data = {"BAD_ROLE": {"grants": {"DATABASE_USAGE": ["MY_DB"]}}} + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + assert any("OBJECT_TYPE:PRIVILEGE" in e for e in result.errors) + + def test_invalid_grant_key_empty_privilege(self): + """A grant key with empty privilege after colon produces an error.""" + data = {"BAD_ROLE": {"grants": {"DATABASE:": ["MY_DB"]}}} + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + + def test_invalid_object_type(self): + """An unrecognised object type in grant key produces an error.""" + data = {"BAD_ROLE": {"grants": {"BANANA:USAGE": ["MY_DB"]}}} + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + assert any("invalid object type" in e for e in result.errors) + + def test_warehouse_cross_reference_valid(self): + """WAREHOUSE grants referencing valid warehouses pass.""" + data = { + "WH_ROLE": { + "grants": {"WAREHOUSE:USAGE": ["COMPUTE_WH"]}, + } + } + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses={"COMPUTE_WH"}) + assert not result.has_errors + + def test_warehouse_cross_reference_invalid(self): + """WAREHOUSE grants referencing unknown warehouses produce an error.""" + data = { + "WH_ROLE": { + "grants": {"WAREHOUSE:USAGE": ["GHOST_WH"]}, + } + } + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses={"COMPUTE_WH"}) + assert result.has_errors + assert any("GHOST_WH" in e for e in result.errors) + + def test_future_grants_validated_too(self): + """future_grants section is validated the same as grants.""" + data = { + "FG_ROLE": { + "future_grants": {"INVALID_OBJ:SELECT": ["MY_DB"]}, + } + } + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + + def test_non_dict_role_entry(self): + """A tech role whose config is not a dict produces an error.""" + data = {"BAD": 42} + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + + def test_duplicate_tech_role_names(self): + """Duplicate tech role names produce an error.""" + data = { + "read_role": {"grants": {}}, + "READ_ROLE": {"grants": {}}, + } + result = ValidationResult() + validate_tech_role_yaml(data, result, warehouses=set()) + assert result.has_errors + assert any("duplicate role name" in e for e in result.errors) + + +# --------------------------------------------------------------------------- +# validate_warehouse_yaml +# --------------------------------------------------------------------------- + + +class TestValidateWarehouseYaml: + """Tests for validate_warehouse_yaml.""" + + def test_valid_warehouse(self): + """A warehouse with valid size and auto_suspend passes.""" + data = { + "COMPUTE_WH": { + "size": "X-Small", + "auto_suspend": 120, + } + } + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert not result.has_errors + + def test_valid_warehouse_sizes(self): + """All valid warehouse sizes pass validation.""" + for size in ["X-Small", "Small", "Medium", "Large", "X-Large", "2X-Large"]: + data = {"WH": {"size": size}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert not result.has_errors, f"Size '{size}' should be valid" + + def test_invalid_warehouse_size(self): + """An invalid warehouse size produces an error.""" + data = {"WH": {"size": "Mega"}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert result.has_errors + assert any("invalid size" in e for e in result.errors) + + def test_auto_suspend_valid(self): + """A valid auto_suspend integer passes.""" + data = {"WH": {"auto_suspend": 60}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert not result.has_errors + + def test_auto_suspend_negative(self): + """A negative auto_suspend produces an error.""" + data = {"WH": {"auto_suspend": -1}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert result.has_errors + assert any("positive integer" in e for e in result.errors) + + def test_auto_suspend_non_integer(self): + """A non-integer auto_suspend produces an error.""" + data = {"WH": {"auto_suspend": "fast"}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert result.has_errors + assert any("must be an integer" in e for e in result.errors) + + def test_auto_suspend_zero(self): + """auto_suspend of 0 is valid (means never auto-suspend).""" + data = {"WH": {"auto_suspend": 0}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert not result.has_errors + + def test_resource_monitor_cross_reference_valid(self): + """A valid resource_monitor reference passes.""" + data = {"WH": {"resource_monitor": "MY_MONITOR"}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors={"MY_MONITOR"}) + assert not result.has_errors + + def test_resource_monitor_cross_reference_invalid(self): + """An unknown resource_monitor reference produces an error.""" + data = {"WH": {"resource_monitor": "GHOST_MONITOR"}} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors={"MY_MONITOR"}) + assert result.has_errors + assert any("GHOST_MONITOR" in e for e in result.errors) + + def test_non_dict_warehouse_entry(self): + """A warehouse whose config is not a dict produces an error.""" + data = {"WH": "string-value"} + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert result.has_errors + + def test_duplicate_warehouse_names(self): + """Duplicate warehouse names produce an error.""" + data = { + "compute_wh": {"size": "Small"}, + "COMPUTE_WH": {"size": "Small"}, + } + result = ValidationResult() + validate_warehouse_yaml(data, result, resource_monitors=set()) + assert result.has_errors + assert any("duplicate warehouse name" in e for e in result.errors) + + +# --------------------------------------------------------------------------- +# validate_network_policy_yaml +# --------------------------------------------------------------------------- + + +class TestValidateNetworkPolicyYaml: + """Tests for validate_network_policy_yaml.""" + + def test_valid_cidr(self): + """Valid CIDR notation in allowed_ip_list passes.""" + data = { + "MY_POLICY": { + "allowed_ip_list": ["10.0.0.0/8", "192.168.1.0/24", "88.216.232.26/32"] + } + } + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert not result.has_errors + + def test_invalid_cidr(self): + """Invalid CIDR notation produces an error.""" + data = {"MY_POLICY": {"allowed_ip_list": ["not-an-ip"]}} + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert result.has_errors + assert any("not valid CIDR" in e for e in result.errors) + + def test_blocked_ip_list_valid(self): + """Valid CIDR in blocked_ip_list passes.""" + data = {"MY_POLICY": {"blocked_ip_list": ["10.0.0.1/32"]}} + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert not result.has_errors + + def test_blocked_ip_list_invalid(self): + """Invalid CIDR in blocked_ip_list produces an error.""" + data = {"MY_POLICY": {"blocked_ip_list": ["garbage"]}} + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert result.has_errors + assert any("blocked IP" in e for e in result.errors) + + def test_non_dict_policy_entry(self): + """A policy whose config is not a dict produces an error.""" + data = {"BAD": 123} + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert result.has_errors + + def test_single_ip_no_cidr_suffix(self): + """A bare IP address (no /mask) is valid CIDR for ip_network(strict=False).""" + data = {"MY_POLICY": {"allowed_ip_list": ["192.168.1.1"]}} + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert not result.has_errors + + def test_duplicate_policy_names(self): + """Duplicate policy names produce an error.""" + data = { + "policy_a": {"allowed_ip_list": ["10.0.0.0/8"]}, + "POLICY_A": {"allowed_ip_list": ["10.0.0.0/8"]}, + } + result = ValidationResult() + validate_network_policy_yaml(data, result) + assert result.has_errors + + +# --------------------------------------------------------------------------- +# validate_resource_monitor_yaml +# --------------------------------------------------------------------------- + + +class TestValidateResourceMonitorYaml: + """Tests for validate_resource_monitor_yaml.""" + + def test_valid_credit_quota(self): + """A positive integer credit_quota passes.""" + data = {"MY_MONITOR": {"credit_quota": 100}} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert not result.has_errors + + def test_invalid_credit_quota_zero(self): + """A credit_quota of 0 produces an error (must be positive).""" + data = {"MY_MONITOR": {"credit_quota": 0}} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert result.has_errors + assert any("must be positive" in e for e in result.errors) + + def test_invalid_credit_quota_negative(self): + """A negative credit_quota produces an error.""" + data = {"MY_MONITOR": {"credit_quota": -50}} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert result.has_errors + + def test_invalid_credit_quota_string(self): + """A non-integer credit_quota produces an error.""" + data = {"MY_MONITOR": {"credit_quota": "lots"}} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert result.has_errors + assert any("must be an integer" in e for e in result.errors) + + def test_no_credit_quota_is_fine(self): + """A monitor without credit_quota is acceptable.""" + data = {"MY_MONITOR": {"frequency": "MONTHLY"}} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert not result.has_errors + + def test_non_dict_monitor_entry(self): + """A monitor whose config is not a dict produces an error.""" + data = {"BAD": "nope"} + result = ValidationResult() + validate_resource_monitor_yaml(data, result) + assert result.has_errors + + +# --------------------------------------------------------------------------- +# _cross_reference_checks +# --------------------------------------------------------------------------- + + +class TestCrossReferenceChecks: + """Tests for _cross_reference_checks.""" + + def _make_loaded(self, **overrides): + """Build a loaded dict with defaults and optional overrides.""" + loaded = { + "user.yaml": {}, + "business_role.yaml": {}, + "tech_role.yaml": {}, + "warehouse.yaml": {}, + "network_policy.yaml": {}, + "resource_monitor.yaml": {}, + } + loaded.update(overrides) + business_roles = {k.upper() for k in loaded["business_role.yaml"]} + tech_roles = {k.upper() for k in loaded["tech_role.yaml"]} + warehouses = {k.upper() for k in loaded["warehouse.yaml"]} + resource_monitors = {k.upper() for k in loaded["resource_monitor.yaml"]} + return loaded, business_roles, tech_roles, warehouses, resource_monitors + + def test_all_valid_cross_refs(self): + """All cross-references resolve -- only ok messages.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "user.yaml": {"ALICE": {"type": "PERSON", "default_role": "ANALYST"}}, + "business_role.yaml": { + "ANALYST": { + "tech_roles": ["READ_ROLE"], + "warehouse_usage": ["COMPUTE_WH"], + } + }, + "tech_role.yaml": { + "READ_ROLE": { + "grants": {"WAREHOUSE:USAGE": ["COMPUTE_WH"]}, + } + }, + "warehouse.yaml": {"COMPUTE_WH": {"size": "Small"}}, + "resource_monitor.yaml": {}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert not result.has_errors + + def test_bad_user_default_role(self): + """User default_role pointing to unknown role is caught.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "user.yaml": {"ALICE": {"type": "PERSON", "default_role": "GHOST"}}, + "business_role.yaml": {"ANALYST": {}}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert result.has_errors + assert any("GHOST" in e for e in result.errors) + + def test_bad_business_role_tech_role_ref(self): + """Business role referencing non-existent tech role is caught.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "business_role.yaml": {"ANALYST": {"tech_roles": ["MISSING"]}}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert result.has_errors + assert any("MISSING" in e for e in result.errors) + + def test_bad_business_role_warehouse_ref(self): + """Business role referencing non-existent warehouse is caught.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "business_role.yaml": {"ANALYST": {"warehouse_usage": ["MISSING_WH"]}}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert result.has_errors + assert any("MISSING_WH" in e for e in result.errors) + + def test_bad_warehouse_resource_monitor_ref(self): + """Warehouse referencing non-existent resource monitor is caught.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "warehouse.yaml": {"WH": {"resource_monitor": "MISSING_MON"}}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert result.has_errors + assert any("MISSING_MON" in e for e in result.errors) + + def test_bad_tech_role_warehouse_grant(self): + """Tech role WAREHOUSE grant referencing unknown warehouse is caught.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "tech_role.yaml": { + "ROLE_X": {"grants": {"WAREHOUSE:USAGE": ["NOPE_WH"]}}, + }, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert result.has_errors + assert any("NOPE_WH" in e for e in result.errors) + + def test_system_role_as_default_role(self): + """System roles like SYSADMIN are accepted as user default_role.""" + loaded, br, tr, wh, rm = self._make_loaded( + **{ + "user.yaml": {"ADMIN": {"type": "PERSON", "default_role": "ACCOUNTADMIN"}}, + } + ) + result = ValidationResult() + _cross_reference_checks(loaded, result, br, tr, wh, rm) + assert not result.has_errors + + +# --------------------------------------------------------------------------- +# run_validation (end-to-end) +# --------------------------------------------------------------------------- + + +class TestRunValidation: + """End-to-end tests for run_validation using temporary config directories.""" + + def _write_yaml(self, path, data): + path.write_text(yaml.dump(data, default_flow_style=False)) + + def test_valid_config_returns_zero(self, tmp_path): + """A fully valid config directory returns exit code 0.""" + self._write_yaml( + tmp_path / "user.yaml", + { + "ALICE": { + "type": "PERSON", + "email": "a@b.com", + "default_role": "ANALYST", + } + }, + ) + self._write_yaml( + tmp_path / "business_role.yaml", + { + "ANALYST": { + "tech_roles": ["READ_ROLE"], + "warehouse_usage": ["COMPUTE_WH"], + } + }, + ) + self._write_yaml( + tmp_path / "tech_role.yaml", + {"READ_ROLE": {"grants": {"DATABASE:USAGE": ["MY_DB"]}}}, + ) + self._write_yaml( + tmp_path / "warehouse.yaml", + {"COMPUTE_WH": {"size": "Small", "auto_suspend": 120}}, + ) + self._write_yaml( + tmp_path / "network_policy.yaml", + {"OFFICE_POLICY": {"allowed_ip_list": ["10.0.0.0/8"]}}, + ) + self._write_yaml( + tmp_path / "resource_monitor.yaml", + {"DAILY_MONITOR": {"credit_quota": 50}}, + ) + + exit_code = run_validation(tmp_path, target_files=None, strict=False, quiet=True) + assert exit_code == 0 + + def test_invalid_config_returns_one(self, tmp_path): + """A config with errors returns exit code 1.""" + self._write_yaml( + tmp_path / "user.yaml", + {"BAD_USER": {"email": "no-type@x.com"}}, # missing type + ) + exit_code = run_validation(tmp_path, target_files=None, strict=False, quiet=True) + assert exit_code == 1 + + def test_warnings_only_returns_zero_without_strict(self, tmp_path): + """Warnings without --strict return exit code 0.""" + self._write_yaml( + tmp_path / "user.yaml", + {"SVC": {"type": "SERVICE"}}, # missing rsa key = warning + ) + exit_code = run_validation(tmp_path, target_files=None, strict=False, quiet=True) + assert exit_code == 0 + + def test_warnings_with_strict_returns_one(self, tmp_path): + """Warnings with --strict return exit code 1.""" + self._write_yaml( + tmp_path / "user.yaml", + {"SVC": {"type": "SERVICE"}}, # missing rsa key = warning + ) + exit_code = run_validation(tmp_path, target_files=None, strict=True, quiet=True) + assert exit_code == 1 + + def test_missing_config_dir_returns_one(self, tmp_path): + """A non-existent config directory returns exit code 1.""" + exit_code = run_validation( + tmp_path / "nonexistent", target_files=None, strict=False, quiet=True + ) + assert exit_code == 1 + + def test_target_specific_file(self, tmp_path): + """Validating a specific target file works.""" + self._write_yaml( + tmp_path / "user.yaml", + {"ALICE": {"type": "PERSON", "email": "a@b.com"}}, + ) + self._write_yaml( + tmp_path / "warehouse.yaml", + {"WH": {"size": "Mega"}}, # invalid size = error + ) + # Only validate user.yaml -- should pass despite warehouse errors + exit_code = run_validation( + tmp_path, + target_files=[str(tmp_path / "user.yaml")], + strict=False, + quiet=True, + ) + assert exit_code == 0 + + def test_empty_config_dir_returns_zero(self, tmp_path): + """An empty config directory (no files) returns 0.""" + exit_code = run_validation(tmp_path, target_files=None, strict=False, quiet=True) + assert exit_code == 0 + + def test_yaml_syntax_error_returns_one(self, tmp_path): + """A file with YAML syntax errors returns exit code 1.""" + (tmp_path / "user.yaml").write_text("key: [broken") + exit_code = run_validation(tmp_path, target_files=None, strict=False, quiet=True) + assert exit_code == 1 diff --git a/uv.lock b/uv.lock index 3a595c6..ff0ef91 100644 --- a/uv.lock +++ b/uv.lock @@ -2360,7 +2360,7 @@ wheels = [ [[package]] name = "snowtower" -version = "0.2.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "click" },