diff --git a/docs/data-sources/azure_external_datasource.md b/docs/data-sources/azure_external_datasource.md new file mode 100644 index 0000000..44e4133 --- /dev/null +++ b/docs/data-sources/azure_external_datasource.md @@ -0,0 +1,67 @@ +-> Functionality is limited to Azure SQL Database only for RDBMS or BLOB_STORAGE type + +# mssql_azure_external_datasource (Data Source) + +The `mssql_azure_external_datasource` obtains information about external data source on an Azure SQL Datatbase. + + +## Example Usage + +```hcl +data "mssql_azure_external_datasource" "rdbms" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "example_db" + data_source_name = "example_name" +} +``` +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Required) The name of the database to operate on. +* `data_source_name` - (Required) The external data source name. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `data_source_id` - The id of this data source name. +* `credential_name` - The name of the database scoped credential. +* `credential_id` - The id of the database scoped credential. +* `location` - The connectivity protocol and path to the external data source. +* `type` - The `type` of a database-scoped credential for authenticating to the external data source. +* `remote_database_name` - The name of the remote database on the server provided using `location`. diff --git a/docs/data-sources/database_credential.md b/docs/data-sources/database_credential.md new file mode 100644 index 0000000..6c6be5e --- /dev/null +++ b/docs/data-sources/database_credential.md @@ -0,0 +1,63 @@ +# mssql_database_credential (Data Source) + +The `mssql_database_credential` obtains information about user permissions on a SQL Server. + +## Example Usage + +```hcl +data "mssql_database_credential" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "xxxxxxxxxxxxxxxxxxxxxx" + } + } + database = "example" + credential_name = "example-credential-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Required) The database. +* `credential_name` - (Required) The database scoped credential name. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database scoped credential. +* `credential_id` - The id of this database scoped credential. +* `credential_name` - The name of the database scoped credential. +* `identity_name` - The name of the account. diff --git a/docs/data-sources/database_permissions.md b/docs/data-sources/database_permissions.md new file mode 100644 index 0000000..72dfcb0 --- /dev/null +++ b/docs/data-sources/database_permissions.md @@ -0,0 +1,61 @@ +# mssql_database_permissions (Data Source) + +The `mssql_database_permissions` obtains information about user permissions on a SQL Server. + +## Example Usage + +```hcl +data "mssql_database_permissions" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "xxxxxxxxxxxxxxxxxxxxxx" + } + } + database = "example" + username = "example-username" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Required) The database. +* `username` - (Required) The name of the database user. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database role. +* `permissions` - List of permissions to grant to the user. diff --git a/docs/data-sources/database_role.md b/docs/data-sources/database_role.md new file mode 100644 index 0000000..8902d22 --- /dev/null +++ b/docs/data-sources/database_role.md @@ -0,0 +1,62 @@ +# mssql_database_role (Data Source) + +The `mssql_database_role` obtains information about database role. + +## Example Usage + +```hcl +data "mssql_database_role" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "xxxxxxxxxxxxxxxxxxxxxx" + } + } + database = "master" + role_name = "example-role-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Optional) The database. Defaults to `master`. +* `role_name` - (Required) The name of the role. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database role. +* `owner_name` - The database user name or role name that is own the role. +* `owning_principal_id` - The database user id or the role id that is own the role. diff --git a/docs/data-sources/database_schema.md b/docs/data-sources/database_schema.md new file mode 100644 index 0000000..0aea713 --- /dev/null +++ b/docs/data-sources/database_schema.md @@ -0,0 +1,62 @@ +# mssql_database_schema (Data Source) + +The `mssql_database_schema` obtains information about database schema. + +## Example Usage + +```hcl +data "mssql_database_schema" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "xxxxxxxxxxxxxxxxxxxxxx" + } + } + database = "my-database" + schema_name = "example-schema-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Optional) The database. Defaults to `master`. +* `schema_name` - (Required) The name of the schema. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `schema_id` - The schema id of this database schema. +* `owner_name` - The database user name or role name that is own the role. +* `owning_principal_id` - The database user id or the role id that is own the role. diff --git a/docs/data-sources/login.md b/docs/data-sources/login.md new file mode 100644 index 0000000..336c001 --- /dev/null +++ b/docs/data-sources/login.md @@ -0,0 +1,59 @@ +# mssql_login (Data Source) + +The `mssql_login` obtains information about SQL login. + +## Example Usage + +```hcl +data "mssql_login" "example" { + server { + host = "example-sql-server.database.windows.net" + login { + username = "sa" + password = "password" + } + } + login_name = "testlogin" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `login_name` - (Required) The name of the server login. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this server login. +* `sid` - The security identifier (SID). +* `default_language` - Default language assigned to login. diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 0000000..3c9e882 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,64 @@ +# mssql_user (Data Source) + +The `mssql_user` obtains information about SQL user. + +## Example Usage + +```hcl +data "mssql_user" "example" { + server { + host = "example-sql-server.database.windows.net" + login { + username = "sa" + password = "password" + } + } + database = "master" + user_name = "testuser" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Optional) The database. Defaults to `master`. +* `username` - (Required) The name of the database user. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database user. +* `sid` - The security identifier (SID). +* `login_name` - The login name of the database user. +* `default_schema` - Schema assigned to this database user. +* `roles` - Database roles the user has. +* `authentication_type` - The authentication type diff --git a/docs/resources/azure_external_datasource.md b/docs/resources/azure_external_datasource.md new file mode 100644 index 0000000..68c7268 --- /dev/null +++ b/docs/resources/azure_external_datasource.md @@ -0,0 +1,109 @@ +-> Functionality is limited to Azure SQL Database only for RDBMS or BLOB_STORAGE type + +# mssql_azure_external_datasource + +The `mssql_azure_external_datasource` resource creates and manages an external data source on a Azure SQL database. + + +## Example Usage + +```hcl +resource "mssql_database_masterkey" "name" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = tenant_id + client_id = client_id + client_secret = client_secret + } + } + + database = "dbname" + password = "strongpassword" +} + +resource "mssql_database_credential" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login {} + } + database = "example-db" + credential_name = "example-credential-name" + identity_name = "example-identity" + secret = "strong secret" +} + +resource "mssql_azure_external_datasource" "rdbms" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "example_db" + data_source_name = "example_name" + location = "remote_server_name.database.windows.net" + credential_name = mssql_database_credential.example.credential_name + type = "RDBMS" + remote_database_name = "remoteDB" +} +``` +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `database` - (Required) The name of the database to operate on. Changing this forces a new resource to be created. +* `data_source_name` - (Required) Specifies the name of the external data source being created. Changing this forces a new resource to be created. +* `location` - (Required) Provides the connectivity protocol and path to the external data source. Changing this resource property modifies the existing resource. +* `credential_name` - (Required) Specifies a database-scoped credential for authenticating to the external data source. +* `type` - (Required) Specifies the type of the external data source being configured. One of either `RDBMS` or `BLOB_STORAGE` must be specified. Changing this forces a new resource to be created. +* `remote_database_name` - (Optional) The name of the remote database on the server provided using `location`. Configure this argument when the `type` is set to `RDBMS`. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `data_source_id` - The id of this data source name. +* `credential_id` - The id of the database scoped credential. + +## Import + +Before importing `mssql_azure_external_datasource`, you must to configure the authentication to your sql server: + +1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. +2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. + +After that you can import the SQL Server database scoped credential using the server URL and `data source name`, e.g. + +```shell +terraform import mssql_azure_external_datasource.example 'mssql://example-sql-server.database.windows.net/example-db/data_source_name' +``` diff --git a/docs/resources/database_credential.md b/docs/resources/database_credential.md new file mode 100644 index 0000000..dba04db --- /dev/null +++ b/docs/resources/database_credential.md @@ -0,0 +1,90 @@ +# mssql_database_credential + +The `mssql_database_credential` resource create a database credential on a SQL Server. + +## Example Usage + +```hcl +resource "mssql_database_masterkey" "name" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = tenant_id + client_id = client_id + client_secret = client_secret + } + } + + database = "dbname" + password = "strongpassword" +} + +resource "mssql_database_credential" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login {} + } + database = "example-db" + credential_name = "example-credential-name" + identity_name = "example-identity" + secret = "strong secret" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. Changing this forces a new resource to be created. +* `database` - (Required) The name of the database to operate on. Changing this forces a new resource to be created. +* `credential_name` - (Required) Specifies the name of the database scoped credential being created. Changing this forces a new resource to be created. +* `identity_name` - (Required) Specifies the name of the account to be used when connecting outside the server. Changing this resource property modifies the existing resource. +* `secret` - (Optional) Specifies the secret required for outgoing authentication. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database scoped credential. +* `credential_id` - The id of this database scoped credential. +* `credential_name` - The name of the database scoped credential. +* `identity_name` - The name of the account. + +## Import + +Before importing `mssql_database_credential`, you must to configure the authentication to your sql server: + +1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. +2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. + +After that you can import the SQL Server database scoped credential using the server URL and `credential name`, e.g. + +```shell +terraform import mssql_database_credential.example 'mssql://example-sql-server.database.windows.net/example-db/credential_name' +``` diff --git a/docs/resources/database_masterkey.md b/docs/resources/database_masterkey.md new file mode 100644 index 0000000..27ece9e --- /dev/null +++ b/docs/resources/database_masterkey.md @@ -0,0 +1,62 @@ +# mssql_database_masterkey + +The `mssql_database_masterkey` resource create a database master key in the database on a SQL Server. + +## Example Usage + +```hcl +resource "mssql_database_masterkey" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login {} + } + database = "example-db" + password = "strong password" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. Changing this forces a new resource to be created. +* `database` - (Required) The name of the database to operate on. Changing this forces a new resource to be created. +* `password` - (Required) The password that is used to encrypt the master key in the database. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database master key. +* `key_name` - The name of this database master key. +* `key_guid` - The guid of this database master key. +* `symmetric_key_id` - The symmetric key id of this database master key. +* `key_length` - The lenth of this database master key. +* `key_algorithm` - The algorithm of this database master key. +* `algorithm_desc` - The algorithm description of this database master key. diff --git a/docs/resources/database_permissions.md b/docs/resources/database_permissions.md new file mode 100644 index 0000000..62f7701 --- /dev/null +++ b/docs/resources/database_permissions.md @@ -0,0 +1,69 @@ +# mssql_database_permissions + +The `mssql_database_permissions` resource manages user permissions on a SQL Server. + +## Example Usage + +```hcl +resource "mssql_database_permissions" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login {} + } + database = "example" + username = "sql_username" + permissions = [ + "EXECUTE", + "UPDATE", + "INSERT", + ] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. Changing this forces a new resource to be created. +* `database` - (Required) The name of the database to operate on. Changing this forces a new resource to be created. +* `username` - (Required) The name of the database user. Changing this forces a new resource to be created. +* `permissions` - (Required) List of permissions to grant to the user. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Import + +Before importing `mssql_database_permissions`, you must to configure the authentication to your sql server: + +1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. +2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. + +After that you can import the SQL Server database permission using the server URL and `user name`, e.g. + +```shell +terraform import mssql_database_permissions.example 'mssql://example-sql-server.database.windows.net/master/username/permissions' +``` diff --git a/docs/resources/database_role.md b/docs/resources/database_role.md new file mode 100644 index 0000000..baa09d3 --- /dev/null +++ b/docs/resources/database_role.md @@ -0,0 +1,96 @@ +# mssql_database_role + +The `mssql_database_role` resource creates and manages a role on a SQL Server database. + +## Example Usage + +### Basic usage + +```hcl +resource "mssql_database_role" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "master" + role_name = "example-role-name" +} +``` + +### Using AUTHORIZATION + +```hcl +resource "mssql_database_role" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "my-database" + role_name = "example-role-name" + owner_name = "example_username" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `role_name` - (Required) The name of the role. Changing this resource property modifies the existing resource. +* `database` - (Optional) The role will be created in this database. Defaults to `master`. Changing this forces a new resource to be created. +* `owner_name` - (Optional) Is the database user or role that is to own the new role. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `principal_id` - The principal id of this database role. +* `owner_name` - The database user name or role name that is own the role. +* `owning_principal_id` - The database user id or the role id that is own the role. + +## Import + +Before importing `mssql_database_role`, you must to configure the authentication to your sql server: + +1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. +2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. + +After that you can import the SQL Server database role using the server URL and `role name`, e.g. + +```shell +terraform import mssql_database_role.example 'mssql://example-sql-server.database.windows.net/master/testrole' +``` diff --git a/docs/resources/database_schema.md b/docs/resources/database_schema.md new file mode 100644 index 0000000..eef4bed --- /dev/null +++ b/docs/resources/database_schema.md @@ -0,0 +1,96 @@ +# mssql_database_role + +The `mssql_database_schema` resource creates and manages a schema on a SQL Server database. + +## Example Usage + +### Basic usage + +```hcl +resource "mssql_database_schema" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "master" + schema_name = "example-schema-name" +} +``` + +### Using AUTHORIZATION + +```hcl +resource "mssql_database_role" "example" { + server { + host = "example-sql-server.database.windows.net" + azure_login { + tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + client_secret = "terriblySecretSecret" + } + } + database = "my-database" + schema_name = "example-schema-name" + owner_name = "example_username" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. +* `schema_name` - (Required) The name of the schema. Changing this forces a new resource to be created. +* `database` - (Optional) The schema will be created in this database. Defaults to `master`. Changing this forces a new resource to be created. +* `owner_name` - (Optional) Is the database user that is to own the new schema. Changing this resource property modifies the existing resource. + +The `server` block supports the following arguments: + +* `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. +* `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. +* `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. +* `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. +* `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. +* `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. + +The `login` block supports the following arguments: + +* `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. +* `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. + +The `azure_login` block supports the following arguments: + +* `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. +* `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. +* `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. + +The `azuread_managed_identity_auth` block supports the following arguments: + +* `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. + +-> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. + +## Attribute Reference + +The following attributes are exported: + +* `schema_id` - The schema id of this database schema. +* `owner_name` - The database user name that is own the schema. +* `owning_principal_id` - The database user id that is own the role. + +## Import + +Before importing `mssql_database_schema`, you must to configure the authentication to your sql server: + +1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. +2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. + +After that you can import the SQL Server database role using the server URL and `role name`, e.g. + +```shell +terraform import mssql_database_schema.example 'mssql://example-sql-server.database.windows.net/master/testschema' +``` diff --git a/examples/azure/main.tf b/examples/azure/main.tf index 87b0b4d..7cfd5f4 100644 --- a/examples/azure/main.tf +++ b/examples/azure/main.tf @@ -276,3 +276,75 @@ output "external" { } sensitive = true } + +resource "mssql_database_role" "example" { + server { + host = azurerm_mssql_server.sql_server.fully_qualified_domain_name + azure_login { + tenant_id = var.tenant_id + client_id = azuread_service_principal.sa.client_id + client_secret = azuread_service_principal_password.sa.value + } + } + database = "master" + role_name = "testrole" +} + +resource "mssql_database_role" "example_authorization" { + server { + host = azurerm_mssql_server.sql_server.fully_qualified_domain_name + azure_login { + tenant_id = var.tenant_id + client_id = azuread_service_principal.sa.client_id + client_secret = azuread_service_principal_password.sa.value + } + } + database = "master" + role_name = "testrole" + owner_name = mssql_user.external.username +} + +resource "mssql_database_permissions" "example" { + server { + host = azurerm_mssql_server.sql_server.fully_qualified_domain_name + azure_login { + tenant_id = var.tenant_id + client_id = azuread_service_principal.sa.client_id + client_secret = azuread_service_principal_password.sa.value + } + } + database = "example" + username = "username" + permissions = [ + "EXECUTE", + "UPDATE", + "INSERT", + ] +} + +resource "mssql_database_schema" "example" { + server { + host = azurerm_mssql_server.sql_server.fully_qualified_domain_name + azure_login { + tenant_id = var.tenant_id + client_id = azuread_service_principal.sa.client_id + client_secret = azuread_service_principal_password.sa.value + } + } + database = "master" + schema_name = "testschema" +} + +resource "mssql_database_schema" "example_authorization" { + server { + host = azurerm_mssql_server.sql_server.fully_qualified_domain_name + azure_login { + tenant_id = var.tenant_id + client_id = azuread_service_principal.sa.client_id + client_secret = azuread_service_principal_password.sa.value + } + } + database = "my-database" + schema_name = "testschema" + owner_name = mssql_user.external.username +} \ No newline at end of file diff --git a/examples/local/main.tf b/examples/local/main.tf index 2caae39..dc8bab2 100644 --- a/examples/local/main.tf +++ b/examples/local/main.tf @@ -113,3 +113,90 @@ output "login" { } sensitive = true } + +data "mssql_login" "example" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + login_name = mssql_login.example.login_name + + depends_on = [mssql_login.example] +} + +output "datalogin" { + value = { + principal_id = data.mssql_login.example.principal_id + sid = data.mssql_login.example.sid + } +} + +resource "mssql_database_role" "example" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + database = "master" + role_name = "testrole" +} + +resource "mssql_database_role" "example_authorization" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + database = "example-db" + role_name = "example-role" + owner_name = mssql_user.example.username +} + +resource "mssql_database_permissions" "example" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + database = "example" + username = "username" + permissions = [ + "EXECUTE", + "UPDATE", + "INSERT", + ] +} + +resource "mssql_database_schema" "example" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + database = "master" + schema_name = "testschema" +} + +resource "mssql_database_schema" "example_authorization" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + database = "example-db" + schema_name = "example-schema" + owner_name = mssql_user.example.username +} \ No newline at end of file diff --git a/mssql/const.go b/mssql/const.go index 7a66496..69c6b0d 100644 --- a/mssql/const.go +++ b/mssql/const.go @@ -1,16 +1,24 @@ package mssql const ( - serverProp = "server" - databaseProp = "database" - principalIdProp = "principal_id" - usernameProp = "username" - objectIdProp = "object_id" - passwordProp = "password" - sidStrProp = "sid" - clientIdProp = "client_id" - authenticationTypeProp = "authentication_type" - defaultSchemaProp = "default_schema" - defaultSchemaPropDefault = "dbo" - rolesProp = "roles" + serverProp = "server" + databaseProp = "database" + principalIdProp = "principal_id" + usernameProp = "username" + objectIdProp = "object_id" + passwordProp = "password" + sidStrProp = "sid" + clientIdProp = "client_id" + authenticationTypeProp = "authentication_type" + defaultSchemaProp = "default_schema" + defaultSchemaPropDefault = "dbo" + rolesProp = "roles" + loginNameProp = "login_name" + permissionsProp = "permissions" + roleNameProp = "role_name" + schemaNameProp = "schema_name" + ownerNameProp = "owner_name" + ownerIdProp = "owning_principal_id" + schemaIdProp = "schema_id" + defaultOwnerNameDefault = "dbo" ) diff --git a/mssql/datasource_azure_external_datasource.go b/mssql/datasource_azure_external_datasource.go new file mode 100644 index 0000000..c688a85 --- /dev/null +++ b/mssql/datasource_azure_external_datasource.go @@ -0,0 +1,121 @@ +package mssql + +import ( + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func datasourceAzureExternalDatasource() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceAzureExternalDatasourceRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + datasourcenameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + datasourceIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + locationProp: { + Type: schema.TypeString, + Computed: true, + }, + credentialNameProp: { + Type: schema.TypeString, + Computed: true, + }, + typedescProp: { + Type: schema.TypeString, + Computed: true, + }, + rdatabasenameProp: { + Type: schema.TypeString, + Computed: true, + }, + credentialIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func datasourceAzureExternalDatasourceRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "azureexternaldatasource", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + mssqlversion, err := connector.GetMSSQLVersion(ctx) + if err != nil { + return diag.FromErr(errors.Wrap(err, "unable to get MSSQL version")) + } + if !strings.Contains(mssqlversion, "Microsoft SQL Azure") { + return diag.Errorf("Error: The database is not an Azure SQL Database.") + } + + datasource, err := connector.GetAzureExternalDatasource(ctx, database, datasourcename) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read external data source [%s] on database [%s]", datasourcename, database)) + } + if datasource == nil { + logger.Info().Msgf("No external data source [%s] found on database [%s]", datasourcename, database) + data.SetId("") + } else { + if err = data.Set(datasourcenameProp, datasource.DataSourceName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(datasourceIdProp, datasource.DataSourceId); err != nil { + return diag.FromErr(err) + } + if err = data.Set(locationProp, datasource.Location); err != nil { + return diag.FromErr(err) + } + if err = data.Set(typedescProp, datasource.TypeDesc); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialNameProp, datasource.CredentialName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialIdProp, datasource.CredentialId); err != nil { + return diag.FromErr(err) + } + if err = data.Set(rdatabasenameProp, datasource.RDatabaseName); err != nil { + return diag.FromErr(err) + } + data.SetId(getAzureExternalDatasourceID(data)) + } + + return nil +} diff --git a/mssql/datasource_azure_external_datasource_test.go b/mssql/datasource_azure_external_datasource_test.go new file mode 100644 index 0000000..2c97b31 --- /dev/null +++ b/mssql/datasource_azure_external_datasource_test.go @@ -0,0 +1,126 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataAzureExternalDatasource_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataAzureExternalDatasourceDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataAzureExternalDatasource(t, "data_azure_test", "azure", map[string]interface{}{"database": "testdb", "data_source_name": "data_test_datasource", "location": "fakesqlsrv.database.windows.net", "type": "RDBMS", "remote_database_name": "test_db_remote", "credential_name": "data_test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/data_test_datasource"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "data_source_name", "data_test_datasource"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_azure_external_datasource.data_azure_test", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_azure_external_datasource.data_azure_test", "data_source_id"), + resource.TestCheckResourceAttrSet("data.mssql_azure_external_datasource.data_azure_test", "credential_id"), + ), + }, + }, + }) +} + +func testAccCheckDataAzureExternalDatasource(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_masterkey" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + password = "{{ .password }}" + } + resource "mssql_database_credential" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + credential_name = "{{ .credential_name }}" + identity_name = "{{ .identity_name }}" + {{ with .secret }}secret = "{{ . }}"{{ end }} + depends_on = [mssql_database_masterkey.{{ .name }}] + } + resource "mssql_azure_external_datasource" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + data_source_name = "{{ .data_source_name }}" + location = "{{ .location }}" + credential_name = "{{ .credential_name }}" + type = "{{ .type }}" + remote_database_name = "{{ .remote_database_name }}" + depends_on = [mssql_database_credential.{{ .name }}] + } + data "mssql_azure_external_datasource" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + data_source_name = "{{ .data_source_name }}" + depends_on = [mssql_azure_external_datasource.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDataAzureExternalDatasourceDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_azure_external_datasource" { + continue + } + if rs.Type != "mssql_database_credential" { + continue + } + if rs.Type != "mssql_database_masterkey" { + continue + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + datasourcename := rs.Primary.Attributes["data_source_name"] + extdatasource, err := connector.GetAzureExternalDatasource(database, datasourcename) + if extdatasource != nil { + return fmt.Errorf("external datasource still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} \ No newline at end of file diff --git a/mssql/datasource_database_credential.go b/mssql/datasource_database_credential.go new file mode 100644 index 0000000..0913d63 --- /dev/null +++ b/mssql/datasource_database_credential.go @@ -0,0 +1,91 @@ +package mssql + +import ( + "context" + + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func datasourceDatabaseCredential() *schema.Resource { + return &schema.Resource{ + ReadContext: datasourceDatabaseCredentialRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + credentialNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + identitynameProp: { + Type: schema.TypeString, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + credentialIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func datasourceDatabaseCredentialRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasecredential", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + scopedcredential, err := connector.GetDatabaseCredential(ctx, database, credentialname) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read database scoped credential [%s] on database [%s]", credentialname, database)) + } + if scopedcredential == nil { + logger.Info().Msgf("No database scoped credential [%s] found on database [%s]", credentialname, database) + data.SetId("") + } else { + if err = data.Set(credentialNameProp, scopedcredential.CredentialName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(identitynameProp, scopedcredential.IdentityName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, scopedcredential.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialIdProp, scopedcredential.CredentialID); err != nil { + return diag.FromErr(err) + } + data.SetId(getDatabaseCredentialID(data)) + } + + return nil +} diff --git a/mssql/datasource_database_credential_test.go b/mssql/datasource_database_credential_test.go new file mode 100644 index 0000000..8b33462 --- /dev/null +++ b/mssql/datasource_database_credential_test.go @@ -0,0 +1,110 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataDatabaseCredential_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataCredentialDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataCredential(t, "data_azure_test", "azure", map[string]interface{}{"database": "testdb", "credential_name": "test_scoped_data_cred", "identity_name": "test_identity_data_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/test_scoped_data_cred"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "credential_name", "test_scoped_data_cred"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_database_credential.data_azure_test", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_credential.data_azure_test", "principal_id"), + resource.TestCheckResourceAttrSet("data.mssql_database_credential.data_azure_test", "credential_id"), + ), + }, + }, + }) +} + +func testAccCheckDataCredential(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_masterkey" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + password = "{{ .password }}" + } + resource "mssql_database_credential" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + credential_name = "{{ .credential_name }}" + identity_name = "{{ .identity_name }}" + {{ with .secret }}secret = "{{ . }}"{{ end }} + depends_on = [mssql_database_masterkey.{{ .name }}] + } + data "mssql_database_credential" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + credential_name = "{{ .credential_name }}" + depends_on = [mssql_database_credential.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDataCredentialDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_credential" { + continue + } + if rs.Type != "mssql_database_masterkey" { + continue + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + credentialName := rs.Primary.Attributes["credential_name"] + scopedcredential, err := connector.GetDatabaseCredential(database, credentialName) + if scopedcredential != nil { + return fmt.Errorf("scoped credential still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} \ No newline at end of file diff --git a/mssql/datasource_database_permissions.go b/mssql/datasource_database_permissions.go new file mode 100644 index 0000000..be290e7 --- /dev/null +++ b/mssql/datasource_database_permissions.go @@ -0,0 +1,90 @@ +package mssql + +import ( + "context" + + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func dataSourceDatabasePermissions() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceDatabasePermissionsRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + usernameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + permissionsProp: { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func dataSourceDatabasePermissionsRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasepermissions", "read") + logger.Debug().Msgf("Read %s", getDatabasePermissionsID(data)) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + permissions, err := connector.GetDatabasePermissions(ctx, database, username) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read permissions for user [%s] on database [%s]", username, database)) + } + if permissions == nil { + logger.Info().Msgf("No permissions found for user [%s] on database [%s]", username, database) + data.SetId("") + } else { + if err = data.Set(databaseProp, permissions.DatabaseName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(usernameProp, permissions.UserName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, permissions.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(permissionsProp, permissions.Permissions); err != nil { + return diag.FromErr(err) + } + data.SetId(getDatabasePermissionsID(data)) + } + + return nil +} diff --git a/mssql/datasource_database_permissions_test.go b/mssql/datasource_database_permissions_test.go new file mode 100644 index 0000000..9b8dffb --- /dev/null +++ b/mssql/datasource_database_permissions_test.go @@ -0,0 +1,157 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataDatabasePermissions_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataDataBasepermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm", "permissions": "[\"REFERENCES\", \"UPDATE\"]", "login_name": "db_login_perm", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "id", "sqlserver://localhost:1433/master/db_user_perm/permissions"), //guess user principal-ID = 7 + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "permissions.#", "2"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "permissions.0", "REFERENCES"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "permissions.1", "UPDATE"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_permissions.database", "principal_id"), + ), + }, + }, + }) +} + +func TestAccDataDatabasePermissions_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataDataBasepermissions(t, "data_azure_test", "azure", map[string]interface{}{"database": "testdb", "username": "azure_user_perm", "permissions": "[\"INSERT\", \"UPDATE\"]", "login_name": "azure_login_perm", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/azure_user_perm/permissions"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "permissions.#", "2"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "permissions.0", "INSERT"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "permissions.1", "UPDATE"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_database_permissions.data_azure_test", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_permissions.data_azure_test", "principal_id"), + ), + }, + }, + }) +} + +func testAccCheckDataDataBasepermissions(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `{{ if .login_name }} + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + {{ with .username }}username = "{{ . }}"{{ end }} + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + } + resource "mssql_database_permissions" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + username = mssql_user.{{ .name }}.username + permissions = {{ .permissions }} + } + data "mssql_database_permissions" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + username = mssql_user.{{ .name }}.username + depends_on = [mssql_database_permissions.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDataDatabasePermissionsDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_permissions" { + continue + } + if rs.Type != "mssql_user" { + continue + } + if rs.Type != "mssql_login" { + continue + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + username := rs.Primary.Attributes["username"] + + permissions, err := connector.GetDatabasePermissions(database, username) + if permissions != nil { + return fmt.Errorf("permissions still exist") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} diff --git a/mssql/datasource_database_role.go b/mssql/datasource_database_role.go new file mode 100644 index 0000000..534e99e --- /dev/null +++ b/mssql/datasource_database_role.go @@ -0,0 +1,92 @@ +package mssql + +import ( + "context" + + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func dataSourceDatabaseRole() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceDatabaseRoleRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "master", + }, + roleNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + ownerNameProp: { + Type: schema.TypeString, + Computed: true, + }, + ownerIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func dataSourceDatabaseRoleRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "role", "read") + logger.Debug().Msgf("Read %s", getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + roleName := data.Get(roleNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + role, err := connector.GetDatabaseRole(ctx, database, roleName) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to get role [%s].[%s]", database, roleName)) + } + + if role == nil { + logger.Info().Msgf("role [%s].[%s] does not exist", database, roleName) + data.SetId("") + } else { + if err = data.Set(principalIdProp, role.RoleID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(roleNameProp, role.RoleName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerNameProp, role.OwnerName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerIdProp, role.OwnerId); err != nil { + return diag.FromErr(err) + } + data.SetId(getDatabaseRoleID(data)) + } + + return nil +} diff --git a/mssql/datasource_database_role_test.go b/mssql/datasource_database_role_test.go new file mode 100644 index 0000000..bc43d96 --- /dev/null +++ b/mssql/datasource_database_role_test.go @@ -0,0 +1,123 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataDatabaseRole_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataRole(t, "data_local_test", "login", map[string]interface{}{"role_name": "data_test_role"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "id", "sqlserver://localhost:1433/master/data_test_role"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "database", "master"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "role_name", "data_test_role"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_local_test", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_role.data_local_test", "principal_id"), + ), + }, + }, + }) +} + +func TestAccDataDatabaseRole_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataRole(t, "data_azure_test", "azure", map[string]interface{}{"database": "testdb", "role_name": "data_test_role"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/data_test_role"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "role_name", "data_test_role"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_database_role.data_azure_test", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_role.data_azure_test", "principal_id"), + ), + }, + }, + }) +} + +func testAccCheckDataRole(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_role" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + role_name = "{{ .role_name }}" + } + data "mssql_database_role" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + role_name = "{{ .role_name }}" + depends_on = [mssql_database_role.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDataRoleDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_role" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + roleName := rs.Primary.Attributes["role_name"] + role, err := connector.GetDatabaseRole(database, roleName) + if role != nil { + return fmt.Errorf("role still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} \ No newline at end of file diff --git a/mssql/datasource_database_schema.go b/mssql/datasource_database_schema.go new file mode 100644 index 0000000..931f3f9 --- /dev/null +++ b/mssql/datasource_database_schema.go @@ -0,0 +1,92 @@ +package mssql + +import ( + "context" + + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func dataSourceDatabaseSchema() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceDatabaseSchemaRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "master", + }, + schemaNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + ownerNameProp: { + Type: schema.TypeString, + Computed: true, + }, + ownerIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + schemaIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func dataSourceDatabaseSchemaRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "schema", "read") + logger.Debug().Msgf("Read %s", getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + sqlschema, err := connector.GetDatabaseSchema(ctx, database, schemaName) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to get schema [%s].[%s]", database, schemaName)) + } + + if sqlschema == nil { + logger.Info().Msgf("schema [%s].[%s] does not exist", database, schemaName) + data.SetId("") + } else { + if err = data.Set(schemaIdProp, sqlschema.SchemaID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(schemaNameProp, sqlschema.SchemaName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerNameProp, sqlschema.OwnerName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerIdProp, sqlschema.OwnerId); err != nil { + return diag.FromErr(err) + } + data.SetId(getDatabaseSchemaID(data)) + } + + return nil +} diff --git a/mssql/datasource_database_schema_test.go b/mssql/datasource_database_schema_test.go new file mode 100644 index 0000000..1c2af52 --- /dev/null +++ b/mssql/datasource_database_schema_test.go @@ -0,0 +1,123 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataDatabaseSchema_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataSchema(t, "data_local_test", "login", map[string]interface{}{"schema_name": "data_test_schema"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "id", "sqlserver://localhost:1433/master/data_test_schema"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "database", "master"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "schema_name", "data_test_schema"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_local_test", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_schema.data_local_test", "schema_id"), + ), + }, + }, + }) +} + +func TestAccDataDatabaseSchema_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDataSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDataSchema(t, "data_azure_test", "azure", map[string]interface{}{"database": "testdb", "schema_name": "data_test_schema"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/data_test_schema"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "schema_name", "data_test_schema"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_database_schema.data_azure_test", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_database_schema.data_azure_test", "schema_id"), + ), + }, + }, + }) +} + +func testAccCheckDataSchema(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_schema" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + schema_name = "{{ .schema_name }}" + } + data "mssql_database_schema" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + schema_name = "{{ .schema_name }}" + depends_on = [mssql_database_schema.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDataSchemaDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_schema" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + schemaName := rs.Primary.Attributes["schema_name"] + sqlschema, err := connector.GetDatabaseSchema(database, schemaName) + if sqlschema != nil { + return fmt.Errorf("schema still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} \ No newline at end of file diff --git a/mssql/datasource_login.go b/mssql/datasource_login.go new file mode 100644 index 0000000..24374c9 --- /dev/null +++ b/mssql/datasource_login.go @@ -0,0 +1,86 @@ +package mssql + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func dataSourceLogin() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceLoginRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + loginNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + sidStrProp: { + Type: schema.TypeString, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + defaultDatabaseProp: { + Type: schema.TypeString, + Computed: true, + }, + defaultLanguageProp: { + Type: schema.TypeString, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func dataSourceLoginRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "login", "read") + logger.Debug().Msgf("Read %s", getLoginID(data)) + + loginName := data.Get(loginNameProp).(string) + + connector, err := getLoginConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + login, err := connector.GetLogin(ctx, loginName) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read login [%s]", loginName)) + } + if login == nil { + logger.Info().Msgf("No login found for [%s]", loginName) + data.SetId("") + } else { + if err = data.Set(principalIdProp, login.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(sidStrProp, login.SIDStr); err != nil { + return diag.FromErr(err) + } + if err = data.Set(defaultDatabaseProp, login.DefaultDatabase); err != nil { + return diag.FromErr(err) + } + if err = data.Set(defaultLanguageProp, login.DefaultLanguage); err != nil { + return diag.FromErr(err) + } + data.SetId(getLoginID(data)) + } + + return nil +} diff --git a/mssql/datasource_login_test.go b/mssql/datasource_login_test.go new file mode 100644 index 0000000..1266ca0 --- /dev/null +++ b/mssql/datasource_login_test.go @@ -0,0 +1,121 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataLogin_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccDataLoginDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccDataLogin(t, "basic", false, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_login.basic", "id", "sqlserver://localhost:1433/login_basic"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "login_name", "login_basic"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_login.basic", "principal_id"), + resource.TestCheckResourceAttrSet("data.mssql_login.basic", "sid"), + ), + }, + }, + }) +} + +func TestAccDataLogin_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccDataLoginDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccDataLogin(t, "basic", true, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_login.basic", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/login_basic"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "login_name", "login_basic"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_login.basic", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_login.basic", "principal_id"), + resource.TestCheckResourceAttrSet("data.mssql_login.basic", "sid"), + ), + }, + }, + }) +} + +func testAccDataLogin(t *testing.T, name string, azure bool, data map[string]interface{}) string { + text := `resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{ if .azure }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .password }}" + {{ with .sid }}sid = "{{ . }}"{{ end }} + {{ with .default_database }}default_database = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + } + data "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{ if .azure }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + depends_on = [mssql_login.{{ .name }}] + }` + data["name"] = name + data["azure"] = azure + if azure { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else { + data["host"] = "localhost" + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccDataLoginDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_login" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + loginName := rs.Primary.Attributes["login_name"] + login, err := connector.GetLogin(loginName) + if login != nil { + return fmt.Errorf("login still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} diff --git a/mssql/datasource_user.go b/mssql/datasource_user.go new file mode 100644 index 0000000..a574cae --- /dev/null +++ b/mssql/datasource_user.go @@ -0,0 +1,121 @@ +package mssql + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func dataSourceUser() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceUserRead, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "master", + }, + usernameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + objectIdProp: { + Type: schema.TypeString, + Computed: true, + }, + loginNameProp: { + Type: schema.TypeString, + Computed: true, + }, + sidStrProp: { + Type: schema.TypeString, + Computed: true, + }, + authenticationTypeProp: { + Type: schema.TypeString, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + defaultSchemaProp: { + Type: schema.TypeString, + Computed: true, + }, + defaultLanguageProp: { + Type: schema.TypeString, + Computed: true, + }, + rolesProp: { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Read: defaultTimeout, + }, + } +} + +func dataSourceUserRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "user", "read") + logger.Debug().Msgf("Read %s", getUserID(data)) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + + connector, err := getUserConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + user, err := connector.GetUser(ctx, database, username) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read user [%s].[%s]", database, username)) + } + if user == nil { + logger.Info().Msgf("No user found for [%s].[%s]", database, username) + data.SetId("") + } else { + if err = data.Set(loginNameProp, user.LoginName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(sidStrProp, user.SIDStr); err != nil { + return diag.FromErr(err) + } + if err = data.Set(authenticationTypeProp, user.AuthType); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, user.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(defaultSchemaProp, user.DefaultSchema); err != nil { + return diag.FromErr(err) + } + if err = data.Set(defaultLanguageProp, user.DefaultLanguage); err != nil { + return diag.FromErr(err) + } + if err = data.Set(rolesProp, user.Roles); err != nil { + return diag.FromErr(err) + } + data.SetId(getUserID(data)) + } + + return nil +} diff --git a/mssql/datasource_user_test.go b/mssql/datasource_user_test.go new file mode 100644 index 0000000..a160481 --- /dev/null +++ b/mssql/datasource_user_test.go @@ -0,0 +1,145 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDataUser_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccDataUserDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccDataUser(t, "basic", "login", map[string]interface{}{"username": "instance", "login_name": "user_instance", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_user.basic", "id", "sqlserver://localhost:1433/master/instance"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "database", "master"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "username", "instance"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "login_name", "user_instance"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "roles.#", "1"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "roles.0", "db_owner"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "principal_id"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "authentication_type"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "sid"), + ), + }, + }, + }) +} + +func TestAccDataUser_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccDataLoginDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccDataUser(t, "basic", "azure", map[string]interface{}{"database": "testdb", "username": "instance", "login_name": "user_instance", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.mssql_user.basic", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/instance"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "database", "testdb"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "username", "instance"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.#", "1"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("data.mssql_user.basic", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "principal_id"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "login_name"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "authentication_type"), + resource.TestCheckResourceAttrSet("data.mssql_user.basic", "sid"), + ), + }, + }, + }) +} + +func testAccDataUser(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `{{ if .login_name }} + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}" + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + } + data "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}" + depends_on = [mssql_user.{{ .name }}] + }` + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccDataUserDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_user" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + username := rs.Primary.Attributes["username"] + user, err := connector.GetUser(database, username) + if user != nil { + return fmt.Errorf("user still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} diff --git a/mssql/model/azure_external_datasource.go b/mssql/model/azure_external_datasource.go new file mode 100644 index 0000000..91e2d2a --- /dev/null +++ b/mssql/model/azure_external_datasource.go @@ -0,0 +1,12 @@ +package model + +type AzureExternalDatasource struct { + DatabaseName string + DataSourceName string + DataSourceId int + Location string + TypeDesc string + CredentialName string + CredentialId int + RDatabaseName string +} \ No newline at end of file diff --git a/mssql/model/database_credential.go b/mssql/model/database_credential.go new file mode 100644 index 0000000..9f21ab4 --- /dev/null +++ b/mssql/model/database_credential.go @@ -0,0 +1,9 @@ +package model + +type DatabaseCredential struct { + DatabaseName string + CredentialName string + IdentityName string + PrincipalID int + CredentialID int +} diff --git a/mssql/model/database_masterkey.go b/mssql/model/database_masterkey.go new file mode 100644 index 0000000..8b940df --- /dev/null +++ b/mssql/model/database_masterkey.go @@ -0,0 +1,13 @@ +package model + +type DatabaseMasterkey struct { + DatabaseName string + Password string + KeyName string + KeyGuid string + SymmetricKeyID int + KeyLength int + KeyAlgorithm string + AlgorithmDesc string + PrincipalID int +} diff --git a/mssql/model/database_permissions.go b/mssql/model/database_permissions.go new file mode 100644 index 0000000..12618f1 --- /dev/null +++ b/mssql/model/database_permissions.go @@ -0,0 +1,8 @@ +package model + +type DatabasePermissions struct { + DatabaseName string + UserName string + PrincipalID int + Permissions []string +} diff --git a/mssql/model/database_role.go b/mssql/model/database_role.go new file mode 100644 index 0000000..96ce444 --- /dev/null +++ b/mssql/model/database_role.go @@ -0,0 +1,9 @@ +package model + +// Role represents a SQL Server role +type DatabaseRole struct { + RoleID int + RoleName string + OwnerName string + OwnerId int +} diff --git a/mssql/model/database_schema.go b/mssql/model/database_schema.go new file mode 100644 index 0000000..cccafb7 --- /dev/null +++ b/mssql/model/database_schema.go @@ -0,0 +1,9 @@ +package model + +// Schema represents a SQL Server schema +type DatabaseSchema struct { + SchemaID int + SchemaName string + OwnerName string + OwnerId int +} diff --git a/mssql/provider.go b/mssql/provider.go index f085a11..6750f5b 100644 --- a/mssql/provider.go +++ b/mssql/provider.go @@ -1,17 +1,18 @@ package mssql import ( - "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "io" - "os" - "github.com/betr-io/terraform-provider-mssql/mssql/model" - "github.com/betr-io/terraform-provider-mssql/sql" - "time" + "context" + "fmt" + "io" + "os" + "time" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/sql" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) type mssqlProvider struct { @@ -46,8 +47,22 @@ func Provider(factory model.ConnectorFactory) *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "mssql_login": resourceLogin(), "mssql_user": resourceUser(), + "mssql_database_permissions": resourceDatabasePermissions(), + "mssql_database_role": resourceDatabaseRole(), + "mssql_database_schema": resourceDatabaseSchema(), + "mssql_database_masterkey": resourceDatabaseMasterkey(), + "mssql_database_credential": resourceDatabaseCredential(), + "mssql_azure_external_datasource": resourceAzureExternalDatasource(), + }, + DataSourcesMap: map[string]*schema.Resource{ + "mssql_login": dataSourceLogin(), + "mssql_user": dataSourceUser(), + "mssql_database_permissions": dataSourceDatabasePermissions(), + "mssql_database_role": dataSourceDatabaseRole(), + "mssql_database_schema": dataSourceDatabaseSchema(), + "mssql_database_credential": datasourceDatabaseCredential(), + "mssql_azure_external_datasource": datasourceAzureExternalDatasource(), }, - DataSourcesMap: map[string]*schema.Resource{}, ConfigureContextFunc: func(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { return providerConfigure(ctx, data, factory) }, diff --git a/mssql/provider_test.go b/mssql/provider_test.go index 3ab9ab4..08499c9 100644 --- a/mssql/provider_test.go +++ b/mssql/provider_test.go @@ -1,19 +1,20 @@ package mssql import ( - "bytes" - "context" - sql2 "database/sql" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "os" - "strconv" - "github.com/betr-io/terraform-provider-mssql/mssql/model" - "github.com/betr-io/terraform-provider-mssql/sql" - "testing" - "text/template" - "time" + "bytes" + "context" + sql2 "database/sql" + "fmt" + "os" + "strconv" + "testing" + "text/template" + "time" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/sql" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) var runLocalAccTests bool @@ -61,6 +62,12 @@ type Check struct { type TestConnector interface { GetLogin(name string) (*model.Login, error) GetUser(database, name string) (*model.User, error) + GetDatabasePermissions(database, name string) (*model.DatabasePermissions, error) + GetDatabaseRole(database, name string) (*model.DatabaseRole, error) + GetDatabaseSchema(database, name string) (*model.DatabaseSchema, error) + GetDatabaseCredential(database, name string) (*model.DatabaseCredential, error) + GetAzureExternalDatasource(database, name string) (*model.AzureExternalDatasource, error) + GetDatabaseMasterkey(database string) (*model.DatabaseMasterkey, error) GetSystemUser() (string, error) GetCurrentUser(database string) (string, string, error) } @@ -158,6 +165,30 @@ func (t testConnector) GetUser(database, name string) (*model.User, error) { return t.c.(UserConnector).GetUser(context.Background(), database, name) } +func (t testConnector) GetDatabasePermissions(database, name string) (*model.DatabasePermissions, error) { + return t.c.(DatabasePermissionsConnector).GetDatabasePermissions(context.Background(), database, name) +} + +func (t testConnector) GetDatabaseRole(database string, roleName string) (*model.DatabaseRole, error) { + return t.c.(DatabaseRoleConnector).GetDatabaseRole(context.Background(), database, roleName) +} + +func (t testConnector) GetDatabaseSchema(database string, schemaName string) (*model.DatabaseSchema, error) { + return t.c.(DatabaseSchemaConnector).GetDatabaseSchema(context.Background(), database, schemaName) +} + +func (t testConnector) GetDatabaseCredential(database, credentialName string) (*model.DatabaseCredential, error) { + return t.c.(DatabaseCredentialConnector).GetDatabaseCredential(context.Background(), database, credentialName) +} + +func (t testConnector) GetAzureExternalDatasource(database, datasourceName string) (*model.AzureExternalDatasource, error) { + return t.c.(AzureExternalDatasourceConnector).GetAzureExternalDatasource(context.Background(), database, datasourceName) +} + +func (t testConnector) GetDatabaseMasterkey(database string) (*model.DatabaseMasterkey, error) { + return t.c.(DatabaseMasterkeyConnector).GetDatabaseMasterkey(context.Background(), database) +} + func (t testConnector) GetSystemUser() (string, error) { var user string err := t.c.(*sql.Connector).QueryRowContext(context.Background(), "SELECT SYSTEM_USER;", func(row *sql2.Row) error { diff --git a/mssql/resource_azure_external_datasource.go b/mssql/resource_azure_external_datasource.go new file mode 100644 index 0000000..21974e6 --- /dev/null +++ b/mssql/resource_azure_external_datasource.go @@ -0,0 +1,314 @@ +package mssql + +import ( + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +const datasourcenameProp = "data_source_name" +const datasourceIdProp = "data_source_id" +const locationProp = "location" +const typedescProp = "type" +const rdatabasenameProp = "remote_database_name" + +func resourceAzureExternalDatasource() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAzureExternalDatasourceCreate, + ReadContext: resourceAzureExternalDatasourceRead, + UpdateContext: resourceAzureExternalDatasourceUpdate, + DeleteContext: resourceAzureExternalDatasourceDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceAzureExternalDatasourceImport, + }, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + datasourcenameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + datasourceIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + locationProp: { + Type: schema.TypeString, + Required: true, + }, + credentialNameProp: { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.SQLIdentifier, + }, + typedescProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLAzureExternalDatasourceType, + }, + rdatabasenameProp: { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.SQLIdentifier, + }, + credentialIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: defaultTimeout, + Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, + }, + } +} + +type AzureExternalDatasourceConnector interface { + GetMSSQLVersion(ctx context.Context) (string, error) + CreateAzureExternalDatasource(ctx context.Context, database, datasourcename, location, credentialname, typedesc, rdatabasename string) error + GetAzureExternalDatasource(ctx context.Context, database, datasourcename string) (*model.AzureExternalDatasource, error) + UpdateAzureExternalDatasource(ctx context.Context, database, datasourcename, location, credentialname, rdatabasename string) error + DeleteAzureExternalDatasource(ctx context.Context, database, datasourcename string) error +} + +func resourceAzureExternalDatasourceCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "azureexternaldatasource", "create") + logger.Debug().Msgf("Create %s", getAzureExternalDatasourceID(data)) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + location := data.Get(locationProp).(string) + credentialname := data.Get(credentialNameProp).(string) + typedesc := data.Get(typedescProp).(string) + rdatabasename := data.Get(rdatabasenameProp).(string) + + if (rdatabasename == "") && (typedesc == "RDBMS") { + return diag.Errorf(rdatabasenameProp + " cannot be empty") + } + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + mssqlversion, err := connector.GetMSSQLVersion(ctx) + if err != nil { + return diag.FromErr(errors.Wrap(err, "unable to get MSSQL version")) + } + if !strings.Contains(mssqlversion, "Microsoft SQL Azure") { + return diag.Errorf("The database is not an Azure SQL Database.") + } + + if err = connector.CreateAzureExternalDatasource(ctx, database, datasourcename, location, credentialname, typedesc, rdatabasename); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create external data source [%s] on database [%s]", datasourcename, database)) + } + + data.SetId(getAzureExternalDatasourceID(data)) + + logger.Info().Msgf("created external data source [%s] on database [%s]", datasourcename, database) + + return resourceAzureExternalDatasourceRead(ctx, data, meta) +} + +func resourceAzureExternalDatasourceRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "azureexternaldatasource", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + mssqlversion, err := connector.GetMSSQLVersion(ctx) + if err != nil { + return diag.FromErr(errors.Wrap(err, "unable to get MSSQL version")) + } + if !strings.Contains(mssqlversion, "Microsoft SQL Azure") { + return diag.Errorf("The database is not an Azure SQL Database.") + } + + extdatasource, err := connector.GetAzureExternalDatasource(ctx, database, datasourcename) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read external data source [%s] on database [%s]", datasourcename, database)) + } + if extdatasource == nil { + logger.Info().Msgf("No external data source [%s] found on database [%s]", datasourcename, database) + data.SetId("") + } else { + if err = data.Set(datasourcenameProp, extdatasource.DataSourceName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(datasourceIdProp, extdatasource.DataSourceId); err != nil { + return diag.FromErr(err) + } + if err = data.Set(locationProp, extdatasource.Location); err != nil { + return diag.FromErr(err) + } + if err = data.Set(typedescProp, extdatasource.TypeDesc); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialNameProp, extdatasource.CredentialName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialIdProp, extdatasource.CredentialId); err != nil { + return diag.FromErr(err) + } + if err = data.Set(rdatabasenameProp, extdatasource.RDatabaseName); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceAzureExternalDatasourceUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "azureexternaldatasource", "update") + logger.Debug().Msgf("Update %s", getDatabaseCredentialID(data)) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + location := data.Get(locationProp).(string) + credentialname := data.Get(credentialNameProp).(string) + rdatabasename := data.Get(rdatabasenameProp).(string) + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.UpdateAzureExternalDatasource(ctx, database, datasourcename, location, credentialname, rdatabasename); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update external data source [%s] on database [%s]", datasourcename, database)) + } + + data.SetId(getAzureExternalDatasourceID(data)) + + logger.Info().Msgf("updated external data source [%s] on database [%s]", datasourcename, database) + + return resourceAzureExternalDatasourceRead(ctx, data, meta) +} + +func resourceAzureExternalDatasourceDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "azureexternaldatasource", "delete") + logger.Debug().Msgf("Delete %s", data.Id()) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.DeleteAzureExternalDatasource(ctx, database, datasourcename); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete external data source [%s] on database [%s]", datasourcename, database)) + } + + data.SetId("") + + logger.Info().Msgf("deleted external data source [%s] on database [%s]", datasourcename, database) + + return nil +} + +func resourceAzureExternalDatasourceImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + logger := loggerFromMeta(meta, "azureexternaldatasource", "import") + logger.Debug().Msgf("Import %s", data.Id()) + + server, u, err := serverFromId(data.Id()) + if err != nil { + return nil, err + } + if err := data.Set(serverProp, server); err != nil { + return nil, err + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 3 { + return nil, errors.New("invalid ID") + } + if err = data.Set(databaseProp, parts[1]); err != nil { + return nil, err + } + if err = data.Set(datasourcenameProp, parts[2]); err != nil { + return nil, err + } + + data.SetId(getAzureExternalDatasourceID(data)) + + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + + connector, err := getAzureExternalDatasourceConnector(meta, data) + if err != nil { + return nil, err + } + + mssqlversion, err := connector.GetMSSQLVersion(ctx) + if err != nil { + return nil, errors.Wrapf(err, "unable to get MSSQL version") + } + if !strings.Contains(mssqlversion, "Microsoft SQL Azure") { + return nil, errors.Wrapf(err, "The database is not an Azure SQL Database.") + } + + extdatasource, err := connector.GetAzureExternalDatasource(ctx, database, datasourcename) + if err != nil { + return nil, errors.Wrapf(err, "unable to import external data source [%s] on database [%s]",datasourcename, database) + } + + if extdatasource == nil { + return nil, errors.Errorf("no external data source found [%s] on database [%s] for import",datasourcename, database) + } + + if err = data.Set(datasourcenameProp, extdatasource.DataSourceName); err != nil { + return nil, err + } + if err = data.Set(locationProp, extdatasource.Location); err != nil { + return nil, err + } + if err = data.Set(typedescProp, extdatasource.TypeDesc); err != nil { + return nil, err + } + if err = data.Set(credentialNameProp, extdatasource.CredentialName); err != nil { + return nil, err + } + if err = data.Set(rdatabasenameProp, extdatasource.RDatabaseName); err != nil { + return nil, err + } + + return []*schema.ResourceData{data}, nil +} + +func getAzureExternalDatasourceConnector(meta interface{}, data *schema.ResourceData) (AzureExternalDatasourceConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(AzureExternalDatasourceConnector), nil +} diff --git a/mssql/resource_azure_external_datasource_import_test.go b/mssql/resource_azure_external_datasource_import_test.go new file mode 100644 index 0000000..068c717 --- /dev/null +++ b/mssql/resource_azure_external_datasource_import_test.go @@ -0,0 +1,30 @@ +package mssql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAzureExternalDatasource_Azure_BasicImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckAzureExternalDatasourceDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckAzureExternalDatasource(t, "test_import", "azure", map[string]interface{}{"database": "testdb", "data_source_name": "test_datasource", "location": "fakesqlsrv1.database.windows.net", "type": "RDBMS", "remote_database_name": "test_db_remote", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureExternalDatasourceExists("mssql_azure_external_datasource.test_import"), + ), + }, + { + ResourceName: "mssql_azure_external_datasource.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccImportStateId("mssql_azure_external_datasource.test_import", true), + }, + }, + }) +} diff --git a/mssql/resource_azure_external_datasource_test.go b/mssql/resource_azure_external_datasource_test.go new file mode 100644 index 0000000..7b1f8d6 --- /dev/null +++ b/mssql/resource_azure_external_datasource_test.go @@ -0,0 +1,212 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccAzureExternalDatasource_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckAzureExternalDatasourceDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckAzureExternalDatasource(t, "test_az_ext_datasource", "azure", map[string]interface{}{"database": "testdb", "data_source_name": "test_datasource", "location": "fakesqlsrv.database.windows.net", "type": "RDBMS", "remote_database_name": "test_db_remote", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureExternalDatasourceExists("mssql_azure_external_datasource.test_az_ext_datasource"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "id", "sqlserver://"+os.Getenv("TF_ACC_SQL_SERVER")+":1433/testdb/test_datasource"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "type", "RDBMS"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "data_source_name", "test_datasource"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.test_az_ext_datasource", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.test_az_ext_datasource", "data_source_id"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.test_az_ext_datasource", "credential_id"), + ), + }, + }, + }) +} + +func TestAccAzureExternalDatasource_Azure_Basic_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckAzureExternalDatasourceDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckAzureExternalDatasource(t, "update", "azure", map[string]interface{}{"database": "testdb", "data_source_name": "test_datasource", "location": "fakesqlsrv1.database.windows.net", "type": "RDBMS", "remote_database_name": "test_db_remote", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureExternalDatasourceExists("mssql_azure_external_datasource.update"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "type", "RDBMS"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "location", "fakesqlsrv1.database.windows.net"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.update", "data_source_id"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.update", "credential_id"), + ), + }, + { + Config: testAccCheckAzureExternalDatasource(t, "update", "azure", map[string]interface{}{"database": "testdb", "data_source_name": "test_datasource", "location": "fakesqlsrv2.database.windows.net", "type": "RDBMS", "remote_database_name": "test_db_remote", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckAzureExternalDatasourceExists("mssql_azure_external_datasource.update"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "type", "RDBMS"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "location", "fakesqlsrv2.database.windows.net"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_azure_external_datasource.update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.update", "data_source_id"), + resource.TestCheckResourceAttrSet("mssql_azure_external_datasource.update", "credential_id"), + ), + }, + }, + }) +} + +func testAccCheckAzureExternalDatasource(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_masterkey" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + password = "{{ .password }}" + } + resource "mssql_database_credential" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + credential_name = "{{ .credential_name }}" + identity_name = "{{ .identity_name }}" + {{ with .secret }}secret = "{{ . }}"{{ end }} + depends_on = [mssql_database_masterkey.{{ .name }}] + } + resource "mssql_azure_external_datasource" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + data_source_name = "{{ .data_source_name }}" + location = "{{ .location }}" + credential_name = "{{ .credential_name }}" + type = "{{ .type }}" + remote_database_name = "{{ .remote_database_name }}" + depends_on = [mssql_database_credential.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckAzureExternalDatasourceDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_azure_external_datasource" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + datasourcename := rs.Primary.Attributes["data_source_name"] + datasource, err := connector.GetAzureExternalDatasource(database, datasourcename) + if datasource != nil { + return fmt.Errorf("external datasource still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckAzureExternalDatasourceExists(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_azure_external_datasource" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_azure_external_datasource", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + database := rs.Primary.Attributes["database"] + datasourcename := rs.Primary.Attributes["data_source_name"] + extdatasource, err := connector.GetAzureExternalDatasource(database, datasourcename) + if err != nil { + return fmt.Errorf("error: %s", err) + } + if extdatasource.DataSourceName != datasourcename { + return fmt.Errorf("expected to be data_source_name %s, got %s", datasourcename, extdatasource.DataSourceName) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "data_source_name": + actual = extdatasource.DataSourceName + case "type": + actual = extdatasource.TypeDesc + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && equal(check.expected, actual) { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_database_credential.go b/mssql/resource_database_credential.go new file mode 100644 index 0000000..fff2f9f --- /dev/null +++ b/mssql/resource_database_credential.go @@ -0,0 +1,260 @@ +package mssql + +import ( + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +const credentialNameProp = "credential_name" +const identitynameProp = "identity_name" +const secretProp = "secret" +const credentialIdProp = "credential_id" + +func resourceDatabaseCredential() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDatabaseCredentialCreate, + ReadContext: resourceDatabaseCredentialRead, + UpdateContext: resourceDatabaseCredentialUpdate, + DeleteContext: resourceDatabaseCredentialDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDatabaseCredentialImport, + }, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + credentialNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + identitynameProp: { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.SQLIdentifier, + }, + secretProp: { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validate.SQLIdentifierPassword, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + credentialIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: defaultTimeout, + Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, + }, + } +} + +type DatabaseCredentialConnector interface { + CreateDatabaseCredential(ctx context.Context, database, credentialname, identityname, secret string) error + GetDatabaseCredential(ctx context.Context, database, credentialname string) (*model.DatabaseCredential, error) + UpdateDatabaseCredential(ctx context.Context, database, credentialname, identityname, secret string) error + DeleteDatabaseCredential(ctx context.Context, database, credentialname string) error +} + +func resourceDatabaseCredentialCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasecredential", "create") + logger.Debug().Msgf("Create %s", getDatabaseCredentialID(data)) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + identityname := data.Get(identitynameProp).(string) + secret := data.Get(secretProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.CreateDatabaseCredential(ctx, database, credentialname, identityname, secret); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create database scoped credential [%s] on database [%s]", credentialname, database)) + } + + data.SetId(getDatabaseCredentialID(data)) + + logger.Info().Msgf("created database scoped credential [%s] on database [%s]", credentialname, database) + + return resourceDatabaseCredentialRead(ctx, data, meta) +} + +func resourceDatabaseCredentialRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasecredential", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + scopedcredential, err := connector.GetDatabaseCredential(ctx, database, credentialname) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read database scoped credential [%s] on database [%s]", credentialname, database)) + } + if scopedcredential == nil { + logger.Info().Msgf("No database scoped credential [%s] found on database [%s]", credentialname, database) + data.SetId("") + } else { + if err = data.Set(credentialNameProp, scopedcredential.CredentialName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(identitynameProp, scopedcredential.IdentityName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, scopedcredential.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(credentialIdProp, scopedcredential.CredentialID); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceDatabaseCredentialUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasecredential", "update") + logger.Debug().Msgf("Update %s", getDatabaseCredentialID(data)) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + identityname := data.Get(identitynameProp).(string) + secret := data.Get(secretProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.UpdateDatabaseCredential(ctx, database, credentialname, identityname, secret); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update database scoped credential [%s] on database [%s]", credentialname, database)) + } + + data.SetId(getDatabaseCredentialID(data)) + + logger.Info().Msgf("updated database scoped credential [%s] on database [%s]", credentialname, database) + + return resourceDatabaseCredentialRead(ctx, data, meta) +} + +func resourceDatabaseCredentialDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasecredential", "delete") + logger.Debug().Msgf("Delete %s", data.Id()) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.DeleteDatabaseCredential(ctx, database, credentialname); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete database scoped credential [%s] on database [%s]", credentialname, database)) + } + + data.SetId("") + + logger.Info().Msgf("deleted database scoped credential [%s] on database [%s]", credentialname, database) + + return nil +} + +func resourceDatabaseCredentialImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + logger := loggerFromMeta(meta, "databasecredential", "import") + logger.Debug().Msgf("Import %s", data.Id()) + + server, u, err := serverFromId(data.Id()) + if err != nil { + return nil, err + } + if err := data.Set(serverProp, server); err != nil { + return nil, err + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 3 { + return nil, errors.New("invalid ID") + } + if err = data.Set(databaseProp, parts[1]); err != nil { + return nil, err + } + if err = data.Set(credentialNameProp, parts[2]); err != nil { + return nil, err + } + + data.SetId(getDatabaseCredentialID(data)) + + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + + connector, err := getDatabaseCredentialConnector(meta, data) + if err != nil { + return nil, err + } + + scopedcredential, err := connector.GetDatabaseCredential(ctx, database, credentialname) + if err != nil { + return nil, errors.Wrapf(err, "unable to get database scoped credential [%s] on database [%s]",credentialname, database) + } + + if scopedcredential == nil { + return nil, errors.Errorf("database scoped credential [%s] on database [%s] does not exist",credentialname, database) + } + + if err = data.Set(credentialNameProp, scopedcredential.CredentialName); err != nil { + return nil, err + } + if err = data.Set(identitynameProp, scopedcredential.IdentityName); err != nil { + return nil, err + } + if err = data.Set(principalIdProp, scopedcredential.PrincipalID); err != nil { + return nil, err + } + if err = data.Set(credentialIdProp, scopedcredential.CredentialID); err != nil { + return nil, err + } + + return []*schema.ResourceData{data}, nil +} + +func getDatabaseCredentialConnector(meta interface{}, data *schema.ResourceData) (DatabaseCredentialConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(DatabaseCredentialConnector), nil +} diff --git a/mssql/resource_database_credential_import_test.go b/mssql/resource_database_credential_import_test.go new file mode 100644 index 0000000..ad0cf05 --- /dev/null +++ b/mssql/resource_database_credential_import_test.go @@ -0,0 +1,31 @@ +package mssql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseCredential_Azure_BasicImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabaseCredemtialDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabaseCredential(t, "test_import", "azure", map[string]interface{}{"database": "testdb", "credential_name": "test_scoped_cred_import", "identity_name": "test_identity_name_import", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseCredemtialExists("mssql_database_credential.test_import"), + ), + }, + { + ResourceName: "mssql_database_credential.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret"}, + ImportStateIdFunc: testAccImportStateId("mssql_database_credential.test_import", true), + }, + }, + }) +} diff --git a/mssql/resource_database_credential_test.go b/mssql/resource_database_credential_test.go new file mode 100644 index 0000000..056df8d --- /dev/null +++ b/mssql/resource_database_credential_test.go @@ -0,0 +1,195 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseCredential_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabaseCredemtialDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabaseCredential(t, "test_credential", "azure", map[string]interface{}{"database": "testdb", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseCredemtialExists("mssql_database_credential.test_credential"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "identity_name", "test_identity_name"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_credential.test_credential", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_credential.test_credential", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_credential.test_credential", "credential_id"), + ), + }, + }, + }) +} + +func TestAccDatabaseCredential_Azure_Basic_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabaseCredemtialDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabaseCredential(t, "update", "azure", map[string]interface{}{"database": "testdb", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name_1", "secret": "V3ryS3cretP@asswd", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseCredemtialExists("mssql_database_credential.update"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "identity_name", "test_identity_name_1"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_credential.update", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_credential.update", "credential_id"), + ), + }, + { + Config: testAccCheckDatabaseCredential(t, "update", "azure", map[string]interface{}{"database": "testdb", "credential_name": "test_scoped_cred", "identity_name": "test_identity_name_2", "password": "V3ryS3cretP@asswd!Key"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseCredemtialExists("mssql_database_credential.update"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "credential_name", "test_scoped_cred"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "identity_name", "test_identity_name_2"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_credential.update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_credential.update", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_credential.update", "credential_id"), + ), + }, + }, + }) +} + +func testAccCheckDatabaseCredential(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_masterkey" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + password = "{{ .password }}" + } + resource "mssql_database_credential" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + credential_name = "{{ .credential_name }}" + identity_name = "{{ .identity_name }}" + {{ with .secret }}secret = "{{ . }}"{{ end }} + depends_on = [mssql_database_masterkey.{{ .name }}] + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDatabaseCredemtialDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_credential" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + credentialname := rs.Primary.Attributes["credential_name"] + scopedcredential, err := connector.GetDatabaseCredential(database, credentialname) + if scopedcredential != nil { + return fmt.Errorf("database scoped credential still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckDatabaseCredemtialExists(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_database_credential" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_database_credential", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + database := rs.Primary.Attributes["database"] + credentialname := rs.Primary.Attributes["credential_name"] + scopedcredential, err := connector.GetDatabaseCredential(database, credentialname) + if err != nil { + return fmt.Errorf("error: %s", err) + } + if scopedcredential.CredentialName != credentialname { + return fmt.Errorf("expected to be credential_name %s, got %s", credentialname, scopedcredential.CredentialName) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "credential_name": + actual = scopedcredential.CredentialName + case "identity_name": + actual = scopedcredential.IdentityName + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && equal(check.expected, actual) { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_database_masterkey.go b/mssql/resource_database_masterkey.go new file mode 100644 index 0000000..1fd4559 --- /dev/null +++ b/mssql/resource_database_masterkey.go @@ -0,0 +1,213 @@ +package mssql + +import ( + "context" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +const keynameProp = "key_name" +const keyguidProp = "key_guid" +const symmetrickeyidProp = "symmetric_key_id" +const keylengthProp = "key_length" +const keyalgorithmProp = "key_algorithm" +const algorithmdescProp = "algorithm_desc" + +func resourceDatabaseMasterkey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDatabaseMasterkeyCreate, + ReadContext: resourceDatabaseMasterkeyRead, + UpdateContext: resourceDatabaseMasterkeyUpdate, + DeleteContext: resourceDatabaseMasterkeyDelete, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + passwordProp: { + Type: schema.TypeString, + Required: true, + Sensitive: true, + ValidateFunc: validate.SQLIdentifierPassword, + }, + keynameProp: { + Type: schema.TypeString, + Computed: true, + }, + keyguidProp: { + Type: schema.TypeString, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + symmetrickeyidProp: { + Type: schema.TypeInt, + Computed: true, + }, + keylengthProp: { + Type: schema.TypeInt, + Computed: true, + }, + keyalgorithmProp: { + Type: schema.TypeString, + Computed: true, + }, + algorithmdescProp: { + Type: schema.TypeString, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: defaultTimeout, + Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, + }, + } +} + +type DatabaseMasterkeyConnector interface { + CreateDatabaseMasterkey(ctx context.Context, database, password string) error + GetDatabaseMasterkey(ctx context.Context, database string) (*model.DatabaseMasterkey, error) + UpdateDatabaseMasterkey(ctx context.Context, database, password string) error + DeleteDatabaseMasterkey(ctx context.Context, database string) error +} + +func resourceDatabaseMasterkeyCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasemasterkey", "create") + logger.Debug().Msgf("Create %s", getDatabaseMasterkeyID(data)) + + database := data.Get(databaseProp).(string) + password := data.Get(passwordProp).(string) + + connector, err := getDatabaseMasterkeyConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.CreateDatabaseMasterkey(ctx, database, password); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create database master key on database [%s]", database)) + } + + data.SetId(getDatabaseMasterkeyID(data)) + + logger.Info().Msgf("created database master key on database [%s]", database) + + return resourceDatabaseMasterkeyRead(ctx, data, meta) +} + +func resourceDatabaseMasterkeyRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasemasterkey", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + + connector, err := getDatabaseMasterkeyConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + masterkey, err := connector.GetDatabaseMasterkey(ctx, database) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read database master key on database [%s]", database)) + } + + if masterkey == nil { + logger.Info().Msgf("No database master key found on database [%s]", database) + data.SetId("") + } else { + if err = data.Set(keynameProp, masterkey.KeyName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, masterkey.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(symmetrickeyidProp, masterkey.SymmetricKeyID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(keylengthProp, masterkey.KeyLength); err != nil { + return diag.FromErr(err) + } + if err = data.Set(keyalgorithmProp, masterkey.KeyAlgorithm); err != nil { + return diag.FromErr(err) + } + if err = data.Set(algorithmdescProp, masterkey.AlgorithmDesc); err != nil { + return diag.FromErr(err) + } + if err = data.Set(keyguidProp, masterkey.KeyGuid); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceDatabaseMasterkeyUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasemasterkey", "update") + logger.Debug().Msgf("Update %s", getDatabaseMasterkeyID(data)) + + database := data.Get(databaseProp).(string) + password := data.Get(passwordProp).(string) + + connector, err := getDatabaseMasterkeyConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.UpdateDatabaseMasterkey(ctx, database, password); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update database key on database [%s]", database)) + } + + data.SetId(getDatabaseMasterkeyID(data)) + + logger.Info().Msgf("updated database master key on database [%s]", database) + + return resourceDatabaseMasterkeyRead(ctx, data, meta) +} + +func resourceDatabaseMasterkeyDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasemasterkey", "delete") + logger.Debug().Msgf("Delete %s", data.Id()) + + database := data.Get(databaseProp).(string) + + connector, err := getDatabaseMasterkeyConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.DeleteDatabaseMasterkey(ctx, database); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete database master key on database [%s]", database)) + } + + data.SetId("") + + logger.Info().Msgf("deleted database master key on database [%s]", database) + + return nil +} + +func getDatabaseMasterkeyConnector(meta interface{}, data *schema.ResourceData) (DatabaseMasterkeyConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(DatabaseMasterkeyConnector), nil +} diff --git a/mssql/resource_database_masterkey_test.go b/mssql/resource_database_masterkey_test.go new file mode 100644 index 0000000..a91cce4 --- /dev/null +++ b/mssql/resource_database_masterkey_test.go @@ -0,0 +1,174 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseMasterkey_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabaseMasterkeyDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabaseMasterkey(t, "local_test_masterkey", "login", map[string]interface{}{"database": "master", "password": "V3ryS3cretP@asswd"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseMasterkeyExists("mssql_database_masterkey.local_test_masterkey"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_masterkey.local_test_masterkey", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.local_test_masterkey", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.local_test_masterkey", "key_guid"), + ), + }, + }, + }) +} + +func TestAccDatabaseMasterkey_Local_Basic_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabaseMasterkeyDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabaseMasterkey(t, "update", "login", map[string]interface{}{"database": "master", "password": "V3ryS3cretP@asswd"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseMasterkeyExists("mssql_database_masterkey.update"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.update", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.update", "key_guid"), + ), + }, + { + Config: testAccCheckDatabaseMasterkey(t, "update", "login", map[string]interface{}{"database": "master", "password": "V3ryS3cretP@asswdUpdated123"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabaseMasterkeyExists("mssql_database_masterkey.update"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_masterkey.update", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.update", "principal_id"), + resource.TestCheckResourceAttrSet("mssql_database_masterkey.update", "key_guid"), + ), + }, + }, + }) +} + +func testAccCheckDatabaseMasterkey(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `resource "mssql_database_masterkey" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + password = "{{ .password }}" + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDatabaseMasterkeyDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_masterkey" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + masterkey, err := connector.GetDatabaseMasterkey(database) + if masterkey != nil { + return fmt.Errorf("database master key still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckDatabaseMasterkeyExists(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_database_masterkey" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_database_masterkey", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + database := rs.Primary.Attributes["database"] + keyguid := rs.Primary.Attributes["key_guid"] + masterkey, err := connector.GetDatabaseMasterkey(database) + if err != nil { + return fmt.Errorf("error: %s", err) + } + if masterkey.KeyGuid != keyguid { + return fmt.Errorf("expected to be key_guid %s, got %s", keyguid, masterkey.KeyGuid) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "key_guid": + actual = masterkey.KeyGuid + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && equal(check.expected, actual) { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_database_permissions.go b/mssql/resource_database_permissions.go new file mode 100644 index 0000000..81dcbf2 --- /dev/null +++ b/mssql/resource_database_permissions.go @@ -0,0 +1,263 @@ +package mssql + +import ( + "context" + "encoding/json" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func resourceDatabasePermissions() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDatabasePermissionsCreate, + ReadContext: resourceDatabasePermissionsRead, + UpdateContext: resourceDatabasePermissionUpdate, + DeleteContext: resourceDatabasePermissionDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDatabasePermissionImport, + }, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + usernameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + permissionsProp: { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Default: defaultTimeout, + Read: defaultTimeout, + }, + } +} + +type DatabasePermissionsConnector interface { + CreateDatabasePermissions(ctx context.Context, dbPermission *model.DatabasePermissions) error + GetDatabasePermissions(ctx context.Context, database string, username string) (*model.DatabasePermissions, error) + UpdateDatabasePermissions(ctx context.Context, dbPermission *model.DatabasePermissions) error + DeleteDatabasePermissions(ctx context.Context, dbPermission *model.DatabasePermissions) error +} + +func resourceDatabasePermissionsCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasepermissions", "create") + logger.Debug().Msgf("Create %s", getDatabasePermissionsID(data)) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + permissions := data.Get(permissionsProp).(*schema.Set).List() + permissions_, _ := json.Marshal(permissions) + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + dbPermissionModel := &model.DatabasePermissions{ + DatabaseName: database, + UserName: username, + Permissions: toStringSlice(permissions), + } + if err = connector.CreateDatabasePermissions(ctx, dbPermissionModel); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create database permissions %v on database [%s] for user [%s]", string(permissions_), database, username)) + } + + data.SetId(getDatabasePermissionsID(data)) + + logger.Info().Msgf("created database permissions %v on database [%s] for user [%s]", string(permissions_), database, username) + + return resourceDatabasePermissionsRead(ctx, data, meta) +} + +func resourceDatabasePermissionsRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasepermissions", "read") + logger.Debug().Msgf("Read %s", data.Id()) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + permissions, err := connector.GetDatabasePermissions(ctx, database, username) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to read permissions for user [%s] on database [%s]", username, database)) + } + if permissions == nil { + logger.Info().Msgf("No permissions found for user [%s] on database [%s]", username, database) + data.SetId("") + } else { + if err = data.Set(databaseProp, permissions.DatabaseName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(usernameProp, permissions.UserName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(principalIdProp, permissions.PrincipalID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(permissionsProp, permissions.Permissions); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceDatabasePermissionDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasepermissions", "delete") + logger.Debug().Msgf("Delete %s", data.Id()) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + permissions := data.Get(permissionsProp).(*schema.Set).List() + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + dbPermissionModel := &model.DatabasePermissions{ + DatabaseName: database, + UserName: username, + Permissions: toStringSlice(permissions), + } + if err = connector.DeleteDatabasePermissions(ctx, dbPermissionModel); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete permissions for user [%s] on database [%s]", username, database)) + } + + data.SetId("") + + logger.Info().Msgf("deleted permissions for user [%s] on database [%s]", username, database) + + return nil +} + +func resourceDatabasePermissionUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "databasepermissions", "update") + logger.Debug().Msgf("Update %s", data.Id()) + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + permissions := data.Get(permissionsProp).(*schema.Set).List() + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + dbPermissionModel := &model.DatabasePermissions{ + DatabaseName: database, + UserName: username, + Permissions: toStringSlice(permissions), + } + if err = connector.UpdateDatabasePermissions(ctx, dbPermissionModel); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update permissions for user [%s] on database [%s]", username, database)) + } + + data.SetId(getDatabasePermissionsID(data)) + + logger.Info().Msgf("updated permissions for user [%s] on database [%s]", username, database) + + return resourceDatabasePermissionsRead(ctx, data, meta) +} + + +func resourceDatabasePermissionImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + logger := loggerFromMeta(meta, "databasepermissions", "import") + logger.Debug().Msgf("Import %s", data.Id()) + + server, u, err := serverFromId(data.Id()) + if err != nil { + return nil, err + } + if err = data.Set(serverProp, server); err != nil { + return nil, err + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 4 { + return nil, errors.New("invalid ID") + } + + if err = data.Set(databaseProp, parts[1]); err != nil { + return nil, err + } + if err = data.Set(usernameProp, parts[2]); err != nil { + return nil, err + } + + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + + data.SetId(getDatabasePermissionsID(data)) + + connector, err := getDatabasePermissionsConnector(meta, data) + if err != nil { + return nil, err + } + + permissions, err := connector.GetDatabasePermissions(ctx, database, username) + if err != nil { + return nil, errors.Wrapf(err, "unable to import permissions for user [%s] on database [%s]", username, database) + } + + if permissions == nil { + return nil, errors.Errorf("no permissions found for user [%s] on database [%s] for import", username, database) + } + + if err = data.Set(databaseProp, permissions.DatabaseName); err != nil { + return nil, err + } + if err = data.Set(usernameProp, permissions.UserName); err != nil { + return nil, err + } + if err = data.Set(principalIdProp, permissions.PrincipalID); err != nil { + return nil, err + } + if err = data.Set(permissionsProp, permissions.Permissions); err != nil { + return nil, err + } + + return []*schema.ResourceData{data}, nil +} + +func getDatabasePermissionsConnector(meta interface{}, data *schema.ResourceData) (DatabasePermissionsConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(DatabasePermissionsConnector), nil +} diff --git a/mssql/resource_database_permissions_import_test.go b/mssql/resource_database_permissions_import_test.go new file mode 100644 index 0000000..d4ffcea --- /dev/null +++ b/mssql/resource_database_permissions_import_test.go @@ -0,0 +1,31 @@ +package mssql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabasePermissions_Local_BasicImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabasePermissions(t, "test_import", "login", map[string]interface{}{"username": "db_user_import", "database":"master", "permissions": "[\"REFERENCES\"]", "login_name": "db_login_import", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.test_import"), + ), + }, + { + ResourceName: "mssql_database_permissions.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccImportStateId("mssql_database_permissions.test_import", false), + }, + }, + }) +} diff --git a/mssql/resource_database_permissions_test.go b/mssql/resource_database_permissions_test.go new file mode 100644 index 0000000..5a9ce04 --- /dev/null +++ b/mssql/resource_database_permissions_test.go @@ -0,0 +1,289 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabasePermissions_Local_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabasePermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm", "permissions": "[\"REFERENCES\", \"UPDATE\"]", "login_name": "db_login_perm", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "2"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "REFERENCES"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.1", "UPDATE"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + }, + }) +} + +func TestAccDatabasePermissions_Local_Basic_update_1(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabasePermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm_grant", "permissions": "[\"EXECUTE\"]", "login_name": "db_login_perm_grant", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "EXECUTE"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + { + Config: testAccCheckDatabasePermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm_grant", "permissions": "[\"EXECUTE\", \"VIEW DEFINITION\"]", "login_name": "db_login_perm_grant", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "2"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "EXECUTE"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.1", "VIEW DEFINITION"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + }, + }) +} + +func TestAccDatabasePermissions_Local_Basic_update_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabasePermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm_update", "permissions": "[\"REFERENCES\",\"UPDATE\"]", "login_name": "db_login_perm_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "2"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "REFERENCES"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.1", "UPDATE"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + { + Config: testAccCheckDatabasePermissions(t, "database", "login", map[string]interface{}{"database": "master", "username": "db_user_perm_update", "permissions": "[\"REFERENCES\"]", "login_name": "db_login_perm_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "REFERENCES"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + }, + }) +} + +func TestAccDatabasePermissions_Azure_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckDatabasePermissionsDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckDatabasePermissions(t, "database", "azure", map[string]interface{}{"database": "testdb", "username": "db_user_perm", "permissions": "[\"EXECUTE\"]", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckDatabasePermissionsExist("mssql_database_permissions.database"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "permissions.0", "EXECUTE"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_permissions.database", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_permissions.database", "principal_id"), + ), + }, + }, + }) +} + +func testAccCheckDatabasePermissions(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `{{ if .login_name }} + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + {{ with .username }}username = "{{ . }}"{{ end }} + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + } + resource "mssql_database_permissions" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + database = "{{ .database }}" + username = mssql_user.{{ .name }}.username + permissions = {{ .permissions }} + }` + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckDatabasePermissionsDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_permissions" { + continue + } + if rs.Type != "mssql_user" { + continue + } + if rs.Type != "mssql_login" { + continue + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + username := rs.Primary.Attributes["username"] + + permissions, err := connector.GetDatabasePermissions(database, username) + if permissions != nil { + return fmt.Errorf("permissions still exist") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckDatabasePermissionsExist(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_database_permissions" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_database_permissions", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + username := rs.Primary.Attributes["username"] + + permissions, err := connector.GetDatabasePermissions(database, username) + if permissions == nil { + return fmt.Errorf("permissions do not exist") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "username": + actual = permissions.UserName + case "permission": + actual = permissions.Permissions + case "database": + actual = permissions.DatabaseName + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && check.expected != actual { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && check.expected == actual { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_database_role.go b/mssql/resource_database_role.go new file mode 100644 index 0000000..ba80f39 --- /dev/null +++ b/mssql/resource_database_role.go @@ -0,0 +1,247 @@ +package mssql + +import ( + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func resourceDatabaseRole() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDatabaseRoleCreate, + ReadContext: resourceDatabaseRoleRead, + UpdateContext: resourceDatabaseRoleUpdate, + DeleteContext: resourceDatabaseRoleDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDatabaseRoleImport, + }, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "master", + }, + roleNameProp: { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.SQLIdentifier, + }, + ownerNameProp: { + Type: schema.TypeString, + Optional: true, + Default: defaultOwnerNameDefault, + DiffSuppressFunc: func(k, old, new string, data *schema.ResourceData) bool { + return (old == "" && new == defaultOwnerNameDefault) || (old == defaultOwnerNameDefault && new == "") + }, + }, + ownerIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + principalIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: defaultTimeout, + Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, + }, + } +} + +type DatabaseRoleConnector interface { + CreateDatabaseRole(ctx context.Context, database string, roleName string, ownerName string) error + GetDatabaseRole(ctx context.Context, database, roleName string) (*model.DatabaseRole, error) + UpdateDatabaseRole(ctx context.Context, database string, roleId int, roleName string, ownerName string) error + DeleteDatabaseRole(ctx context.Context, database, roleName string) error +} + +func resourceDatabaseRoleRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "role", "read") + logger.Debug().Msgf("Read %s", getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + roleName := data.Get(roleNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + role, err := connector.GetDatabaseRole(ctx, database, roleName) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to get role [%s].[%s]", database, roleName)) + } + + if role == nil { + logger.Info().Msgf("role [%s].[%s] does not exist", database, roleName) + data.SetId("") + } else { + if err = data.Set(principalIdProp, role.RoleID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(roleNameProp, role.RoleName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerNameProp, role.OwnerName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerIdProp, role.OwnerId); err != nil { + return diag.FromErr(err) + } + } + + logger.Info().Msgf("read role [%s].[%s]", database, roleName) + + return nil +} + +func resourceDatabaseRoleCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "role", "create") + logger.Debug().Msgf("Create %s", getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + roleName := data.Get(roleNameProp).(string) + ownerName := data.Get(ownerNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.CreateDatabaseRole(ctx, database, roleName, ownerName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create role [%s].[%s]", database, roleName)) + } + + data.SetId(getDatabaseRoleID(data)) + + logger.Info().Msgf("created role [%s].[%s]", database, roleName) + + return resourceDatabaseRoleRead(ctx, data, meta) +} + +func resourceDatabaseRoleDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "role", "delete") + logger.Debug().Msgf("Delete %s", getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + roleName := data.Get(roleNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.DeleteDatabaseRole(ctx, database, roleName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete role [%s].[%s]", database, roleName)) + } + + data.SetId("") + + logger.Info().Msgf("deleted role [%s].[%s]", database, roleName) + + return nil +} + +func resourceDatabaseRoleUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "role", "update") + logger.Debug().Msgf("Update %s", getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + roleId := data.Get(principalIdProp).(int) + roleName := data.Get(roleNameProp).(string) + ownerName := data.Get(ownerNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.UpdateDatabaseRole(ctx, database, roleId, roleName, ownerName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update role [%s].[%s]", database, roleName)) + } + + data.SetId(getDatabaseRoleID(data)) + + logger.Info().Msgf("updated role [%s].[%s]", database, roleName) + + return resourceDatabaseRoleRead(ctx, data, meta) +} + +func resourceDatabaseRoleImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + logger := loggerFromMeta(meta, "role", "import") + logger.Debug().Msgf("Import %s", data.Id()) + + server, u, err := serverFromId(data.Id()) + if err != nil { + return nil, err + } + if err := data.Set(serverProp, server); err != nil { + return nil, err + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 3 { + return nil, errors.New("invalid ID") + } + if err = data.Set(databaseProp, parts[1]); err != nil { + return nil, err + } + if err = data.Set(roleNameProp, parts[2]); err != nil { + return nil, err + } + + data.SetId(getDatabaseRoleID(data)) + + database := data.Get(databaseProp).(string) + role_name := data.Get(roleNameProp).(string) + + connector, err := getDatabaseRoleConnector(meta, data) + if err != nil { + return nil, err + } + + role, err := connector.GetDatabaseRole(ctx, database, role_name) + if err != nil { + return nil, errors.Wrapf(err, "unable to get role [%s].[%s]", database, role_name) + } + + if role == nil { + return nil, errors.Errorf("role [%s].[%s] does not exist", database, role_name) + } + + if err = data.Set(principalIdProp, role.RoleID); err != nil { + return nil, err + } + if err = data.Set(ownerNameProp, role.OwnerName); err != nil { + return nil, err + } + + return []*schema.ResourceData{data}, nil +} + +func getDatabaseRoleConnector(meta interface{}, data *schema.ResourceData) (DatabaseRoleConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(DatabaseRoleConnector), nil +} diff --git a/mssql/resource_database_role_import_test.go b/mssql/resource_database_role_import_test.go new file mode 100644 index 0000000..d09dbc7 --- /dev/null +++ b/mssql/resource_database_role_import_test.go @@ -0,0 +1,31 @@ +package mssql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseRole_Local_BasicImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "test_import", "login", map[string]interface{}{"role_name": "test-role-import"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.test_import"), + ), + }, + { + ResourceName: "mssql_database_role.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccImportStateId("mssql_database_role.test_import", false), + }, + }, + }) +} diff --git a/mssql/resource_database_role_test.go b/mssql/resource_database_role_test.go new file mode 100644 index 0000000..0d5e081 --- /dev/null +++ b/mssql/resource_database_role_test.go @@ -0,0 +1,425 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseRole_Local_Basic_Create(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "local_test_create", "login", map[string]interface{}{"role_name": "test_role_create"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_create"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "role_name", "test_role_create"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_role.local_test_create", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.local_test_create", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.local_test_create", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Local_Basic_Create_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "test_create_auth", "login", map[string]interface{}{"database": "master", "role_name": "test_role_auth", "owner_name": "db_user_role", "username": "db_user_role", "login_name": "db_login_role", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.test_create_auth"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "role_name", "test_role_auth"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "owner_name", "db_user_role"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_role.test_create_auth", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.test_create_auth", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.test_create_auth", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Azure_Basic_Create(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "azure_test_create", "azure", map[string]interface{}{"database":"testdb", "role_name": "test_role_create"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_create"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "role_name", "test_role_create"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_create", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_create", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Azure_Basic_Create_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "azure_test_create_auth", "azure", map[string]interface{}{"database": "testdb", "role_name": "test_role_auth", "owner_name": "db_user_role", "username": "db_user_role", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_create_auth"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "role_name", "test_role_auth"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "owner_name", "db_user_role"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_create_auth", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_create_auth", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_create_auth", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Local_Basic_Update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "local_test_update", "login", map[string]interface{}{"role_name": "test_role_pre"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update", Check{"role_name", "==", "test_role_pre"}), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update", "role_name", "test_role_pre"), + ), + }, + { + Config: testAccCheckRole(t, "local_test_update", "login", map[string]interface{}{"role_name": "test_role_post"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update", Check{"role_name", "==", "test_role_post"}), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update", "role_name", "test_role_post"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Local_Basic_Update_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "local_test_update_auth", "login", map[string]interface{}{"database": "master", "role_name": "test_role_owner", "owner_name": "db_user_owner_role_pre", "username": "db_user_owner_role_pre", "login_name": "db_login_owner_pre", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update_auth", Check{"owner_name", "==", "db_user_owner_role_pre"}), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_auth", "role_name", "test_role_owner"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_auth", "owner_name", "db_user_owner_role_pre"), + ), + }, + { + Config: testAccCheckRole(t, "local_test_update_auth", "login", map[string]interface{}{"database": "master", "role_name": "test_role_owner", "owner_name": "db_user_owner_role_post", "username": "db_user_owner_role_post", "login_name": "db_login_owner_post", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update_auth", Check{"owner_name", "==", "db_user_owner_role_post"}), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_auth", "role_name", "test_role_owner"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_auth", "owner_name", "db_user_owner_role_post"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Local_Basic_Remove_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "local_test_update_rm_auth", "login", map[string]interface{}{"database": "master", "role_name": "test_role_owner_rm", "owner_name": "db_user_owner_rm", "username": "db_user_owner_rm", "login_name": "db_login_owner_rm", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update_rm_auth"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_rm_auth", "role_name", "test_role_owner_rm"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_rm_auth", "owner_name", "db_user_owner_rm"), + ), + }, + { + Config: testAccCheckRole(t, "local_test_update_rm_auth", "login", map[string]interface{}{"database": "master", "role_name": "test_role_owner_rm", "owner_name": "", "username": "db_user_owner_rm", "login_name": "db_login_owner_rm", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.local_test_update_rm_auth"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_rm_auth", "role_name", "test_role_owner_rm"), + resource.TestCheckResourceAttr("mssql_database_role.local_test_update_rm_auth", "owner_name", "dbo"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Azure_Basic_Update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "azure_test_update", "azure", map[string]interface{}{"database":"testdb", "role_name": "test_role_pre"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_update"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "role_name", "test_role_pre"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_update", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_update", "password"), + ), + }, + { + Config: testAccCheckRole(t, "azure_test_update", "azure", map[string]interface{}{"database":"testdb", "role_name": "test_role_post"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_update"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "role_name", "test_role_post"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_update", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_update", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseRole_Azure_Basic_Update_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckRoleDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckRole(t, "azure_test_update_owner", "azure", map[string]interface{}{"database": "testdb", "role_name": "test_role_auth", "owner_name": "db_user_role", "username": "db_user_role", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_update_owner"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "role_name", "test_role_auth"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "owner_name", "db_user_role"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_update_owner", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_update_owner", "password"), + ), + }, + { + Config: testAccCheckRole(t, "azure_test_update_owner", "azure", map[string]interface{}{"database": "testdb", "role_name": "test_role_auth", "username": "db_user_role", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoleExists("mssql_database_role.azure_test_update_owner"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "role_name", "test_role_auth"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "owner_name", "dbo"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_role.azure_test_update_owner", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_role.azure_test_update_owner", "principal_id"), + resource.TestCheckNoResourceAttr("mssql_database_role.azure_test_update_owner", "password"), + ), + }, + }, + }) +} + +func testAccCheckRole(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `{{ if .login_name }} + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + {{ if .username }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}" + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + } + {{ end }} + resource "mssql_database_role" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + role_name = "{{ .role_name }}" + {{ with .owner_name }}owner_name = "{{ . }}"{{ end }} + {{ if .username }} + depends_on = [mssql_user.{{ .name }}] + {{ end }} + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckRoleDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_role" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + roleName := rs.Primary.Attributes["role_name"] + role, err := connector.GetDatabaseRole(database, roleName) + if role != nil { + return fmt.Errorf("role still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckRoleExists(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_database_role" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_database_role", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + database := rs.Primary.Attributes["database"] + roleName := rs.Primary.Attributes["role_name"] + role, err := connector.GetDatabaseRole(database, roleName) + if err != nil { + return fmt.Errorf("error: %s", err) + } + if role.RoleName != roleName { + return fmt.Errorf("expected to be role %s, got %s", roleName, role.RoleName) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "role_name": + actual = role.RoleName + case "owner_name": + actual = role.OwnerName + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && equal(check.expected, actual) { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_database_schema.go b/mssql/resource_database_schema.go new file mode 100644 index 0000000..5ac93fc --- /dev/null +++ b/mssql/resource_database_schema.go @@ -0,0 +1,247 @@ +package mssql + +import ( + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" +) + +func resourceDatabaseSchema() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDatabaseSchemaCreate, + ReadContext: resourceDatabaseSchemaRead, + UpdateContext: resourceDatabaseSchemaUpdate, + DeleteContext: resourceDatabaseSchemaDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDatabaseSchemaImport, + }, + Schema: map[string]*schema.Schema{ + serverProp: { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: getServerSchema(serverProp), + }, + }, + databaseProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "master", + }, + schemaNameProp: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.SQLIdentifier, + }, + ownerNameProp: { + Type: schema.TypeString, + Optional: true, + Default: defaultOwnerNameDefault, + DiffSuppressFunc: func(k, old, new string, data *schema.ResourceData) bool { + return (old == "" && new == defaultOwnerNameDefault) || (old == defaultOwnerNameDefault && new == "") + }, + }, + schemaIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + ownerIdProp: { + Type: schema.TypeInt, + Computed: true, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: defaultTimeout, + Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, + }, + } +} + +type DatabaseSchemaConnector interface { + CreateDatabaseSchema(ctx context.Context, database string, schemaName string, ownerName string) error + GetDatabaseSchema(ctx context.Context, database, schemaName string) (*model.DatabaseSchema, error) + UpdateDatabaseSchema(ctx context.Context, database string, schemaName string, ownerName string) error + DeleteDatabaseSchema(ctx context.Context, database, schemaName string) error +} + +func resourceDatabaseSchemaRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "schema", "read") + logger.Debug().Msgf("Read %s", getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + sqlschema, err := connector.GetDatabaseSchema(ctx, database, schemaName) + if err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to get schema [%s].[%s]", database, schemaName)) + } + + if sqlschema == nil { + logger.Info().Msgf("schema [%s].[%s] does not exist", database, schemaName) + data.SetId("") + } else { + if err = data.Set(schemaIdProp, sqlschema.SchemaID); err != nil { + return diag.FromErr(err) + } + if err = data.Set(schemaNameProp, sqlschema.SchemaName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerNameProp, sqlschema.OwnerName); err != nil { + return diag.FromErr(err) + } + if err = data.Set(ownerIdProp, sqlschema.OwnerId); err != nil { + return diag.FromErr(err) + } + } + + logger.Info().Msgf("read schema [%s].[%s]", database, schemaName) + + return nil +} + +func resourceDatabaseSchemaCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "schema", "create") + logger.Debug().Msgf("Create %s", getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + ownerName := data.Get(ownerNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.CreateDatabaseSchema(ctx, database, schemaName, ownerName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to create schema [%s].[%s]", database, schemaName)) + } + + data.SetId(getDatabaseSchemaID(data)) + + logger.Info().Msgf("created schema [%s].[%s]", database, schemaName) + + return resourceDatabaseSchemaRead(ctx, data, meta) +} + +func resourceDatabaseSchemaDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "schema", "delete") + logger.Debug().Msgf("Delete %s", getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.DeleteDatabaseSchema(ctx, database, schemaName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to delete schema [%s].[%s]", database, schemaName)) + } + + data.SetId("") + + logger.Info().Msgf("deleted schema [%s].[%s]", database, schemaName) + + return nil +} + +func resourceDatabaseSchemaUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + logger := loggerFromMeta(meta, "schema", "update") + logger.Debug().Msgf("Update %s", getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + ownerName := data.Get(ownerNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return diag.FromErr(err) + } + + if err = connector.UpdateDatabaseSchema(ctx, database, schemaName, ownerName); err != nil { + return diag.FromErr(errors.Wrapf(err, "unable to update schema [%s].[%s]", database, schemaName)) + } + + data.SetId(getDatabaseSchemaID(data)) + + logger.Info().Msgf("updated schema [%s].[%s]", database, schemaName) + + return resourceDatabaseSchemaRead(ctx, data, meta) +} + +func resourceDatabaseSchemaImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + logger := loggerFromMeta(meta, "schema", "import") + logger.Debug().Msgf("Import %s", data.Id()) + + server, u, err := serverFromId(data.Id()) + if err != nil { + return nil, err + } + if err := data.Set(serverProp, server); err != nil { + return nil, err + } + + parts := strings.Split(u.Path, "/") + if len(parts) != 3 { + return nil, errors.New("invalid ID") + } + if err = data.Set(databaseProp, parts[1]); err != nil { + return nil, err + } + if err = data.Set(schemaNameProp, parts[2]); err != nil { + return nil, err + } + + data.SetId(getDatabaseSchemaID(data)) + + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + + connector, err := getDatabaseSchemaConnector(meta, data) + if err != nil { + return nil, err + } + + sqlschema, err := connector.GetDatabaseSchema(ctx, database, schemaName) + if err != nil { + return nil, errors.Wrapf(err, "unable to get schema [%s].[%s]", database, schemaName) + } + + if sqlschema == nil { + return nil, errors.Errorf("schema [%s].[%s] does not exist", database, schemaName) + } + + if err = data.Set(schemaIdProp, sqlschema.SchemaID); err != nil { + return nil, err + } + if err = data.Set(ownerNameProp, sqlschema.OwnerName); err != nil { + return nil, err + } + + return []*schema.ResourceData{data}, nil +} + +func getDatabaseSchemaConnector(meta interface{}, data *schema.ResourceData) (DatabaseSchemaConnector, error) { + provider := meta.(model.Provider) + connector, err := provider.GetConnector(serverProp, data) + if err != nil { + return nil, err + } + return connector.(DatabaseSchemaConnector), nil +} diff --git a/mssql/resource_database_schema_import_test.go b/mssql/resource_database_schema_import_test.go new file mode 100644 index 0000000..9a9114f --- /dev/null +++ b/mssql/resource_database_schema_import_test.go @@ -0,0 +1,31 @@ +package mssql + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseSchema_Local_BasicImport(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "test_import", "login", map[string]interface{}{"schema_name": "test_schema_import"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.test_import"), + ), + }, + { + ResourceName: "mssql_database_schema.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccImportStateId("mssql_database_schema.test_import", false), + }, + }, + }) +} diff --git a/mssql/resource_database_schema_test.go b/mssql/resource_database_schema_test.go new file mode 100644 index 0000000..9a78f2c --- /dev/null +++ b/mssql/resource_database_schema_test.go @@ -0,0 +1,352 @@ +package mssql + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccDatabaseSchema_Local_Basic_Create(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "local_test_create", "login", map[string]interface{}{"schema_name": "test_schema_create"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.local_test_create"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "schema_name", "test_schema_create"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_create", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.local_test_create", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.local_test_create", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Local_Basic_Create_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "test_create_auth", "login", map[string]interface{}{"database": "master", "schema_name": "test_schema_auth", "owner_name": "db_user_schema", "username": "db_user_schema", "login_name": "db_login_schema", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.test_create_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "database", "master"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "schema_name", "test_schema_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "owner_name", "db_user_schema"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_database_schema.test_create_auth", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.test_create_auth", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.test_create_auth", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Azure_Basic_Create(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "azure_test_create", "azure", map[string]interface{}{"database":"testdb", "schema_name": "test_schema_create"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.azure_test_create"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "schema_name", "test_schema_create"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.azure_test_create", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.azure_test_create", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Azure_Basic_Create_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "azure_test_create_auth", "azure", map[string]interface{}{"database": "testdb", "schema_name": "test_schema_auth", "owner_name": "db_user_schema", "username": "db_user_schema", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.azure_test_create_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "schema_name", "test_schema_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "owner_name", "db_user_schema"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_create_auth", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.azure_test_create_auth", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.azure_test_create_auth", "password"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Local_Basic_Update_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "local_test_update_auth", "login", map[string]interface{}{"database": "master", "schema_name": "test_schema_owner", "owner_name": "db_user_owner_schema_pre", "username": "db_user_owner_schema_pre", "login_name": "db_login_owner_schema_pre", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.local_test_update_auth", Check{"owner_name", "==", "db_user_owner_schema_pre"}), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_auth", "schema_name", "test_schema_owner"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_auth", "owner_name", "db_user_owner_schema_pre"), + ), + }, + { + Config: testAccCheckSchema(t, "local_test_update_auth", "login", map[string]interface{}{"database": "master", "schema_name": "test_schema_owner", "owner_name": "db_user_owner_schema_post", "username": "db_user_owner_schema_post", "login_name": "db_login_owner_schema_post", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.local_test_update_auth", Check{"owner_name", "==", "db_user_owner_schema_post"}), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_auth", "schema_name", "test_schema_owner"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_auth", "owner_name", "db_user_owner_schema_post"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Local_Basic_Update_remove_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "local_test_update_rm_auth", "login", map[string]interface{}{"database": "master", "schema_name": "test_schema_owner_rm", "owner_name": "db_user_owner_rm", "username": "db_user_owner_rm", "login_name": "db_login_owner_rm", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.local_test_update_rm_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_rm_auth", "schema_name", "test_schema_owner_rm"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_rm_auth", "owner_name", "db_user_owner_rm"), + ), + }, + { + Config: testAccCheckSchema(t, "local_test_update_rm_auth", "login", map[string]interface{}{"database": "master", "schema_name": "test_schema_owner_rm", "owner_name": "", "username": "db_user_owner_rm", "login_name": "db_login_owner_rm", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.local_test_update_rm_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_rm_auth", "schema_name", "test_schema_owner_rm"), + resource.TestCheckResourceAttr("mssql_database_schema.local_test_update_rm_auth", "owner_name", "dbo"), + ), + }, + }, + }) +} + +func TestAccDatabaseSchema_Azure_Basic_Update_owner(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckSchemaDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckSchema(t, "azure_test_update_owner", "azure", map[string]interface{}{"database": "testdb", "schema_name": "test_schema_auth", "owner_name": "db_user_schema", "username": "db_user_schema", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.azure_test_update_owner"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "schema_name", "test_schema_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "owner_name", "db_user_schema"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.azure_test_update_owner", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.azure_test_update_owner", "password"), + ), + }, + { + Config: testAccCheckSchema(t, "azure_test_update_owner", "azure", map[string]interface{}{"database": "testdb", "schema_name": "test_schema_auth", "username": "db_user_schema", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckSchemaExists("mssql_database_schema.azure_test_update_owner"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "database", "testdb"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "schema_name", "test_schema_auth"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "owner_name", "dbo"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_database_schema.azure_test_update_owner", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_database_schema.azure_test_update_owner", "schema_id"), + resource.TestCheckNoResourceAttr("mssql_database_schema.azure_test_update_owner", "password"), + ), + }, + }, + }) +} + +func testAccCheckSchema(t *testing.T, name string, login string, data map[string]interface{}) string { + text := `{{ if .login_name }} + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + {{ if .username }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}" + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + } + {{ end }} + resource "mssql_database_schema" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + schema_name = "{{ .schema_name }}" + {{ with .owner_name }}owner_name = "{{ . }}"{{ end }} + {{ if .username }} + depends_on = [mssql_user.{{ .name }}] + {{ end }} + }` + + data["name"] = name + data["login"] = login + if login == "fedauth" || login == "msi" || login == "azure" { + data["host"] = os.Getenv("TF_ACC_SQL_SERVER") + } else if login == "login" { + data["host"] = "localhost" + } else { + t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) + } + res, err := templateToString(name, text, data) + if err != nil { + t.Fatalf("%s", err) + } + return res +} + +func testAccCheckSchemaDestroy(state *terraform.State) error { + for _, rs := range state.RootModule().Resources { + if rs.Type != "mssql_database_schema" { + continue + } + + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + + database := rs.Primary.Attributes["database"] + schemaName := rs.Primary.Attributes["schema_name"] + sqlschema, err := connector.GetDatabaseSchema(database, schemaName) + if sqlschema != nil { + return fmt.Errorf("schema still exists") + } + if err != nil { + return fmt.Errorf("expected no error, got %s", err) + } + } + return nil +} + +func testAccCheckSchemaExists(resource string, checks ...Check) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Type != "mssql_database_schema" { + return fmt.Errorf("expected resource of type %s, got %s", "mssql_database_schema", rs.Type) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no record ID is set") + } + connector, err := getTestConnector(rs.Primary.Attributes) + if err != nil { + return err + } + database := rs.Primary.Attributes["database"] + schemaName := rs.Primary.Attributes["schema_name"] + sqlschema, err := connector.GetDatabaseSchema(database, schemaName) + if err != nil { + return fmt.Errorf("error: %s", err) + } + if sqlschema.SchemaName != schemaName { + return fmt.Errorf("expected to be schema %s, got %s", schemaName, sqlschema.SchemaName) + } + + var actual interface{} + for _, check := range checks { + switch check.name { + case "schema_name": + actual = sqlschema.SchemaName + case "owner_name": + actual = sqlschema.OwnerName + default: + return fmt.Errorf("unknown property %s", check.name) + } + if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { + return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) + } + if check.op == "!=" && equal(check.expected, actual) { + return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) + } + } + return nil + } +} diff --git a/mssql/resource_login.go b/mssql/resource_login.go index 3e803fb..8c82701 100644 --- a/mssql/resource_login.go +++ b/mssql/resource_login.go @@ -1,15 +1,16 @@ package mssql import ( - "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/pkg/errors" - "strings" - "github.com/betr-io/terraform-provider-mssql/mssql/model" + "context" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" ) -const loginNameProp = "login_name" const defaultDatabaseProp = "default_database" const defaultDatabaseDefault = "master" const defaultLanguageProp = "default_language" @@ -43,11 +44,13 @@ func resourceLogin() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, + ValidateFunc: validate.SQLIdentifier, }, passwordProp: { Type: schema.TypeString, Required: true, Sensitive: true, + ValidateFunc: validate.SQLIdentifierPassword, }, sidStrProp: { Type: schema.TypeString, @@ -76,8 +79,10 @@ func resourceLogin() *schema.Resource { }, }, Timeouts: &schema.ResourceTimeout{ - Default: defaultTimeout, + Create: defaultTimeout, Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, }, } } diff --git a/mssql/resource_login_import_test.go b/mssql/resource_login_import_test.go index 764c324..6fe194b 100644 --- a/mssql/resource_login_import_test.go +++ b/mssql/resource_login_import_test.go @@ -1,9 +1,10 @@ package mssql import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "testing" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccLogin_Local_BasicImport(t *testing.T) { diff --git a/mssql/resource_login_test.go b/mssql/resource_login_test.go index 7c067c4..527d437 100644 --- a/mssql/resource_login_test.go +++ b/mssql/resource_login_test.go @@ -1,11 +1,12 @@ package mssql import ( - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "os" - "testing" + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccLogin_Local_Basic(t *testing.T) { diff --git a/mssql/resource_user.go b/mssql/resource_user.go index 896cf37..c61e775 100644 --- a/mssql/resource_user.go +++ b/mssql/resource_user.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/betr-io/terraform-provider-mssql/mssql/validate" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/pkg/errors" @@ -38,6 +39,7 @@ func resourceUser() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, + ValidateFunc: validate.SQLIdentifier, }, objectIdProp: { Type: schema.TypeString, @@ -48,12 +50,15 @@ func resourceUser() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + ConflictsWith: []string{passwordProp}, + ValidateFunc: validate.SQLIdentifier, }, passwordProp: { Type: schema.TypeString, Optional: true, ForceNew: true, Sensitive: true, + ValidateFunc: validate.SQLIdentifierPassword, }, sidStrProp: { Type: schema.TypeString, @@ -71,6 +76,7 @@ func resourceUser() *schema.Resource { Type: schema.TypeString, Optional: true, Default: defaultSchemaPropDefault, + ValidateFunc: validate.SQLIdentifier, }, defaultLanguageProp: { Type: schema.TypeString, @@ -88,8 +94,10 @@ func resourceUser() *schema.Resource { }, }, Timeouts: &schema.ResourceTimeout{ - Default: defaultTimeout, + Create: defaultTimeout, Read: defaultTimeout, + Update: defaultTimeout, + Delete: defaultTimeout, }, } } @@ -114,9 +122,6 @@ func resourceUserCreate(ctx context.Context, data *schema.ResourceData, meta int defaultLanguage := data.Get(defaultLanguageProp).(string) roles := data.Get(rolesProp).(*schema.Set).List() - if loginName != "" && password != "" { - return diag.Errorf(loginNameProp + " and " + passwordProp + " cannot both be set") - } var authType string if loginName != "" { authType = "INSTANCE" @@ -125,9 +130,6 @@ func resourceUserCreate(ctx context.Context, data *schema.ResourceData, meta int } else { authType = "EXTERNAL" } - if defaultSchema == "" { - return diag.Errorf(defaultSchemaProp + " cannot be empty") - } connector, err := getUserConnector(meta, data) if err != nil { @@ -326,11 +328,3 @@ func getUserConnector(meta interface{}, data *schema.ResourceData) (UserConnecto } return connector.(UserConnector), nil } - -func toStringSlice(values []interface{}) []string { - result := make([]string, len(values)) - for i, v := range values { - result[i] = v.(string) - } - return result -} diff --git a/mssql/resource_user_import_test.go b/mssql/resource_user_import_test.go index a4c47aa..e3bcafc 100644 --- a/mssql/resource_user_import_test.go +++ b/mssql/resource_user_import_test.go @@ -1,30 +1,31 @@ package mssql import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "testing" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccUser_Local_BasicImport(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - IsUnitTest: runLocalAccTests, - ProviderFactories: testAccProviders, - CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, - Steps: []resource.TestStep{ - { - Config: testAccCheckUser(t, "test_import", "login", map[string]interface{}{"username": "user_import", "login_name": "user_import", "login_password": "valueIsH8kd$¡"}), - Check: resource.ComposeTestCheckFunc( - testAccCheckUserExists("mssql_user.test_import"), - ), - }, - { - ResourceName: "mssql_user.test_import", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccImportStateId("mssql_user.test_import", false), - }, - }, - }) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckUser(t, "test_import", "login", map[string]interface{}{"username": "user_import", "login_name": "user_import", "login_password": "valueIsH8kd$¡"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists("mssql_user.test_import"), + ), + }, + { + ResourceName: "mssql_user.test_import", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccImportStateId("mssql_user.test_import", false), + }, + }, + }) } diff --git a/mssql/resource_user_test.go b/mssql/resource_user_test.go index 4ef8e7c..1190d4f 100644 --- a/mssql/resource_user_test.go +++ b/mssql/resource_user_test.go @@ -455,28 +455,28 @@ func TestAccUser_Azure_Update_Roles(t *testing.T) { func testAccCheckUser(t *testing.T, name string, login string, data map[string]interface{}) string { text := `{{ if .login_name }} - resource "mssql_login" "{{ .name }}" { - server { - host = "{{ .host }}" - {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} - } - login_name = "{{ .login_name }}" - password = "{{ .login_password }}" - } - {{ end }} - resource "mssql_user" "{{ .name }}" { - server { - host = "{{ .host }}" - {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} - } - {{ with .database }}database = "{{ . }}"{{ end }} - username = "{{ .username }}" - {{ with .password }}password = "{{ . }}"{{ end }} - {{ with .login_name }}login_name = "{{ . }}"{{ end }} - {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} - {{ with .default_language }}default_language = "{{ . }}"{{ end }} - {{ with .roles }}roles = {{ . }}{{ end }} - }` + resource "mssql_login" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}" + password = "{{ .login_password }}" + } + {{ end }} + resource "mssql_user" "{{ .name }}" { + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}" + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + }` data["name"] = name data["login"] = login if login == "fedauth" || login == "msi" || login == "azure" { @@ -495,30 +495,30 @@ func testAccCheckUser(t *testing.T, name string, login string, data map[string]i func testAccCheckMultipleUsers(t *testing.T, name string, login string, data map[string]interface{}, count int) string { text := `{{ if .login_name }} - resource "mssql_login" "{{ .name }}" { - count = {{ .count }} - server { - host = "{{ .host }}" - {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} - } - login_name = "{{ .login_name }}-${count.index}" - password = "{{ .login_password }}" - } - {{ end }} - resource "mssql_user" "{{ .name }}" { - count = {{ .count }} - server { - host = "{{ .host }}" - {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} - } - {{ with .database }}database = "{{ . }}"{{ end }} - username = "{{ .username }}-${count.index}" - {{ with .password }}password = "{{ . }}"{{ end }} - {{ with .login_name }}login_name = "{{ . }}-${count.index}"{{ end }} - {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} - {{ with .default_language }}default_language = "{{ . }}"{{ end }} - {{ with .roles }}roles = {{ . }}{{ end }} - }` + resource "mssql_login" "{{ .name }}" { + count = {{ .count }} + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + login_name = "{{ .login_name }}-${count.index}" + password = "{{ .login_password }}" + } + {{ end }} + resource "mssql_user" "{{ .name }}" { + count = {{ .count }} + server { + host = "{{ .host }}" + {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} + } + {{ with .database }}database = "{{ . }}"{{ end }} + username = "{{ .username }}-${count.index}" + {{ with .password }}password = "{{ . }}"{{ end }} + {{ with .login_name }}login_name = "{{ . }}-${count.index}"{{ end }} + {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} + {{ with .default_language }}default_language = "{{ . }}"{{ end }} + {{ with .roles }}roles = {{ . }}{{ end }} + }` data["name"] = name data["login"] = login data["count"] = count diff --git a/mssql/utils.go b/mssql/utils.go index 2145ed0..5ba94ed 100644 --- a/mssql/utils.go +++ b/mssql/utils.go @@ -1,10 +1,11 @@ package mssql import ( - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/rs/zerolog" - "github.com/betr-io/terraform-provider-mssql/mssql/model" + "fmt" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/rs/zerolog" ) func getLoginID(data *schema.ResourceData) string { @@ -22,6 +23,61 @@ func getUserID(data *schema.ResourceData) string { return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, username) } +func getDatabasePermissionsID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + username := data.Get(usernameProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s/%s", host, port, database, username, "permissions") +} + +func getDatabaseRoleID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + roleName := data.Get(roleNameProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, roleName) +} + +func getDatabaseSchemaID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + schemaName := data.Get(schemaNameProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, schemaName) +} + +func getDatabaseCredentialID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + credentialname := data.Get(credentialNameProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, credentialname) +} + +func getDatabaseMasterkeyID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, "masterkey") +} + +func getAzureExternalDatasourceID(data *schema.ResourceData) string { + host := data.Get(serverProp + ".0.host").(string) + port := data.Get(serverProp + ".0.port").(string) + database := data.Get(databaseProp).(string) + datasourcename := data.Get(datasourcenameProp).(string) + return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, datasourcename) +} + func loggerFromMeta(meta interface{}, resource, function string) zerolog.Logger { return meta.(model.Provider).ResourceLogger(resource, function) } + +func toStringSlice(values []interface{}) []string { + result := make([]string, len(values)) + for i, v := range values { + result[i] = v.(string) + } + return result +} diff --git a/mssql/validate/validators.go b/mssql/validate/validators.go new file mode 100644 index 0000000..6402a99 --- /dev/null +++ b/mssql/validate/validators.go @@ -0,0 +1,72 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func SQLIdentifier(i interface{}, k string) (warnings []string, errors []error) { + v := i.(string) + if (!regexp.MustCompile(`^[a-zA-Z0-9_.@#-]+$`).MatchString(v)) && (!regexp.MustCompile("SHARED ACCESS SIGNATURE").MatchString(v)) { + errors = append(errors, fmt.Errorf( + "invalid SQL identifier. SQL identifier allows letters, digits, @, $, #, . or _, start with letter, _, @ or # .Got %q", v)) + } + + if 1 > len(v) { + errors = append(errors, fmt.Errorf("%q cannot be less than 1 character: %q", k, v)) + } + + if len(v) > 128 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 128 characters: %q %d", k, v, len(v))) + } + + return +} + +func SQLIdentifierPassword(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return + } + + if len(v) < 8 { + errors = append(errors, fmt.Errorf("length should equal to or greater than %d, got %q", 8, v)) + return + } + + if len(v) > 128 { + errors = append(errors, fmt.Errorf("length should be equal to or less than %d, got %q", 128, v)) + return + } + + switch { + case regexp.MustCompile(`^.*[a-z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[A-Z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[0-9]+.*$`).MatchString(v): + return + case regexp.MustCompile(`^.*[a-z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[A-Z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[\W]+.*$`).MatchString(v): + return + case regexp.MustCompile(`^.*[a-z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[\W]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[0-9]+.*$`).MatchString(v): + return + case regexp.MustCompile(`^.*[A-Z]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[\W]+.*$`).MatchString(v) && regexp.MustCompile(`^.*[0-9]+.*$`).MatchString(v): + return + default: + errors = append(errors, fmt.Errorf("%q must contain characters from three of the categories - uppercase letters, lowercase letters, numbers and non-alphanumeric characters, got %v", k, v)) + return + } +} + +func SQLAzureExternalDatasourceType(i interface{}, k string) (warnings []string, errors []error) { + v := i.(string) + found := false + for _, w := range []string{"BLOB_STORAGE", "RDBMS"} { + if v == w { + found = true + } + } + if !found { + errors = append(errors, fmt.Errorf( + "type must be one of BLOB_STORAGE or RDBMS. Got %q", v)) + } + + return +} diff --git a/sql/azure_external_datasource.go b/sql/azure_external_datasource.go new file mode 100644 index 0000000..f350d97 --- /dev/null +++ b/sql/azure_external_datasource.go @@ -0,0 +1,98 @@ +package sql + +import ( + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetMSSQLVersion(ctx context.Context) (string, error) { + var version string + err := c. + QueryRowContext(ctx, + "SELECT @@VERSION", + func(r *sql.Row) error { + return r.Scan(&version) + }, + ) + if err != nil { + if err == sql.ErrNoRows { + return "", nil + } + return "", err + } + return version, nil +} + +func (c *Connector) GetAzureExternalDatasource(ctx context.Context, database, datasourcename string) (*model.AzureExternalDatasource, error) { + var extds model.AzureExternalDatasource + var rdbname sql.NullString + err := c. + setDatabase(&database). + QueryRowContext(ctx, + "SELECT eds.name, eds.data_source_id, eds.location, eds.type_desc, dsc.name, eds.credential_id, eds.database_name FROM [sys].[external_data_sources] eds INNER JOIN [sys].[database_scoped_credentials] dsc ON dsc.credential_id = eds.credential_id AND eds.name = @datasourcename", + func(r *sql.Row) error { + return r.Scan(&extds.DataSourceName, &extds.DataSourceId, &extds.Location, &extds.TypeDesc, &extds.CredentialName, &extds.CredentialId, &rdbname) + }, + sql.Named("datasourcename", datasourcename), + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + extds.RDatabaseName = rdbname.String + return &extds, nil +} + +func (c *Connector) CreateAzureExternalDatasource(ctx context.Context, database, datasourcename, location, credentialname, typedesc, rdatabasename string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'CREATE EXTERNAL DATA SOURCE ' + QuoteName(@datasourcename) + ' WITH (LOCATION = ' + QuoteName(@location, '''') + ', CREDENTIAL = ' + QuoteName(@credentialname) + ', TYPE = ' + @typedesc + IF @rdatabasename != '' + BEGIN + SET @stmt = @stmt + ', DATABASE_NAME = ' + QuoteName(@rdatabasename, '''') + END + SET @stmt = @stmt + ')' + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("datasourcename", datasourcename), + sql.Named("location", location), + sql.Named("credentialname", credentialname), + sql.Named("typedesc", typedesc), + sql.Named("rdatabasename", rdatabasename), + ) +} + +func (c *Connector) UpdateAzureExternalDatasource(ctx context.Context, database, datasourcename, location, credentialname, rdatabasename string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'ALTER EXTERNAL DATA SOURCE ' + QuoteName(@datasourcename) + ' SET LOCATION = ' + QuoteName(@location, '''') + ', CREDENTIAL = ' + QuoteName(@credentialname) + IF @rdatabasename != '' + BEGIN + SET @stmt = @stmt + ', DATABASE_NAME = ' + QuoteName(@rdatabasename, '''') + END + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("datasourcename", datasourcename), + sql.Named("location", location), + sql.Named("credentialname", credentialname), + sql.Named("rdatabasename", rdatabasename), + ) +} + +func (c *Connector) DeleteAzureExternalDatasource(ctx context.Context, database, datasourcename string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'IF EXISTS (SELECT 1 FROM [sys].[external_data_sources] WHERE [name] = ' + QuoteName(@datasourcename, '''') + ') ' + + 'DROP EXTERNAL DATA SOURCE ' + QuoteName(@datasourcename) + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("datasourcename", datasourcename), + ) +} \ No newline at end of file diff --git a/sql/database_credential.go b/sql/database_credential.go new file mode 100644 index 0000000..d3c510f --- /dev/null +++ b/sql/database_credential.go @@ -0,0 +1,70 @@ +package sql + +import ( + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetDatabaseCredential(ctx context.Context, database, credentialname string) (*model.DatabaseCredential, error) { + var scopedcredential model.DatabaseCredential + err := c. + setDatabase(&database). + QueryRowContext(ctx, + "SELECT name, principal_id, credential_id, credential_identity FROM [sys].[database_scoped_credentials] WHERE [name] = @credentialname", + func(r *sql.Row) error { + return r.Scan(&scopedcredential.CredentialName, &scopedcredential.PrincipalID, &scopedcredential.CredentialID, &scopedcredential.IdentityName) + }, + sql.Named("credentialname", credentialname), + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &scopedcredential, nil +} + +func (c *Connector) CreateDatabaseCredential(ctx context.Context, database, credentialname, identityname, secret string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'CREATE DATABASE SCOPED CREDENTIAL ' + QuoteName(@credentialname) + ' WITH IDENTITY = ' + QuoteName(@identityname, '''') + ', SECRET = ' + QuoteName(@secret, '''') + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("credentialname", credentialname), + sql.Named("identityname", identityname), + sql.Named("secret", secret), + ) +} + +func (c *Connector) UpdateDatabaseCredential(ctx context.Context, database, credentialname, identityname, secret string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'ALTER DATABASE SCOPED CREDENTIAL ' + QuoteName(@credentialname) + ' WITH IDENTITY = ' + QuoteName(@identityname, '''') + IF @secret != '' + BEGIN + SET @stmt = @stmt + ', SECRET = ' + QuoteName(@secret, '''') + END + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("credentialname", credentialname), + sql.Named("identityname", identityname), + sql.Named("secret", secret), + ) +} + +func (c *Connector) DeleteDatabaseCredential(ctx context.Context, database, credentialname string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'IF EXISTS (SELECT 1 FROM [sys].[database_scoped_credentials] WHERE [name] = ' + QuoteName(@credentialname, '''') + ') ' + + 'DROP DATABASE SCOPED CREDENTIAL ' + QuoteName(@credentialname) + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("credentialname", credentialname), + ) +} \ No newline at end of file diff --git a/sql/database_masterkey.go b/sql/database_masterkey.go new file mode 100644 index 0000000..84986d9 --- /dev/null +++ b/sql/database_masterkey.go @@ -0,0 +1,60 @@ +package sql + +import ( + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetDatabaseMasterkey(ctx context.Context, database string) (*model.DatabaseMasterkey, error) { + var masterkey model.DatabaseMasterkey + err := c. + setDatabase(&database). + QueryRowContext(ctx, + "SELECT name, principal_id, symmetric_key_id, key_length, key_algorithm, algorithm_desc, CONVERT(VARCHAR(85), [key_guid], 1) FROM [sys].[symmetric_keys] WHERE name = '##MS_DatabaseMasterKey##'", + func(r *sql.Row) error { + return r.Scan(&masterkey.KeyName, &masterkey.PrincipalID, &masterkey.SymmetricKeyID, &masterkey.KeyLength, &masterkey.KeyAlgorithm, &masterkey.AlgorithmDesc, &masterkey.KeyGuid) + }, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &masterkey, nil +} + +func (c *Connector) CreateDatabaseMasterkey(ctx context.Context, database, password string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'CREATE MASTER KEY ENCRYPTION BY PASSWORD = ' + QuoteName(@password, '''') + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("password", password), + ) +} + +func (c *Connector) UpdateDatabaseMasterkey(ctx context.Context, database, password string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'ALTER MASTER KEY REGENERATE WITH ENCRYPTION BY PASSWORD = ' + QuoteName(@password, '''') + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("password", password), + ) +} + +func (c *Connector) DeleteDatabaseMasterkey(ctx context.Context, database string) error { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'IF EXISTS (SELECT 1 FROM [sys].[symmetric_keys] WHERE name = ' + QuoteName('##MS_DatabaseMasterKey##', '''') + ') ' + + 'DROP MASTER KEY' + EXEC (@stmt)` + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + ) +} diff --git a/sql/database_permissions.go b/sql/database_permissions.go new file mode 100644 index 0000000..58a9b32 --- /dev/null +++ b/sql/database_permissions.go @@ -0,0 +1,146 @@ +package sql + +import ( + "context" + "database/sql" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetDatabasePermissions(ctx context.Context, database string, username string) (*model.DatabasePermissions, error) { + cmd := `DECLARE @stmt nvarchar(max) + SET @stmt = 'SELECT DISTINCT pr.principal_id, pr.name, ' + + 'pe.permission_name ' + + 'FROM [sys].[database_principals] AS pr LEFT JOIN [sys].[database_permissions] AS pe ' + + 'ON pe.grantee_principal_id = pr.principal_id ' + + 'WHERE pr.name = ' + QuoteName(@username, '''') + EXEC (@stmt)` + var ( + permissions []string + ) + + permsModel := model.DatabasePermissions{ + UserName: username, + DatabaseName: database, + Permissions: make([]string, 0), + } + + err := c. + setDatabase(&database). + QueryContext(ctx, cmd, + func(r *sql.Rows) error { + for r.Next() { + var name, permission_name string + var principal_id string + if err := r.Scan(&principal_id, &name, &permission_name); err != nil { + // Check for a scan error. + // Query rows will be closed with defer. + return err + } + if permission_name == "CONNECT" { + continue + } + permissions = append(permissions, permission_name) + } + return nil + }, + sql.Named("database", database), + sql.Named("username", username), + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + + if len(permissions) == 0 { + permsModel.Permissions = make([]string, 0) + } else { + permsModel.Permissions = permissions + } + return &permsModel, nil +} + +func (c *Connector) CreateDatabasePermissions(ctx context.Context, permissions *model.DatabasePermissions) error { + cmd := `DECLARE @stmt nvarchar(max) + DECLARE perm_cur CURSOR FOR SELECT value FROM String_Split(@permissions, ',') + DECLARE @permission_name nvarchar(max) + OPEN perm_cur + FETCH NEXT FROM perm_cur INTO @permission_name + WHILE @@FETCH_STATUS = 0 + BEGIN + SET @stmt = 'GRANT ' + @permission_name + ' TO ' + QuoteName(@username) + EXEC (@stmt) + FETCH NEXT FROM perm_cur INTO @permission_name + END + CLOSE perm_cur + DEALLOCATE perm_cur + ` + return c. + setDatabase(&permissions.DatabaseName). + ExecContext(ctx, cmd, + sql.Named("username", permissions.UserName), + sql.Named("permissions", strings.Join(permissions.Permissions, ",")), + ) +} + +func (c *Connector) UpdateDatabasePermissions(ctx context.Context, permissions *model.DatabasePermissions) error { + cmd := `DECLARE @stmt nvarchar(max) + DECLARE grant_perm_cur CURSOR FOR SELECT value FROM String_Split(@permissions, ',') WHERE value NOT IN(SELECT permission_name FROM [sys].[database_permissions] pe, [sys].[database_principals] pr WHERE pe.grantee_principal_id = pr.principal_id AND pr.name = @username) + DECLARE revoke_perm_cur CURSOR FOR SELECT pe.permission_name FROM [sys].[database_principals] pr LEFT JOIN [sys].[database_permissions] pe ON pe.grantee_principal_id = pr.principal_id AND pr.name = @username AND pe.permission_name != 'CONNECT' AND pe.permission_name NOT IN (SELECT value FROM String_Split(@permissions, ',')) + DECLARE @perm_name nvarchar(max) + + OPEN grant_perm_cur + FETCH NEXT FROM grant_perm_cur INTO @perm_name + WHILE @@FETCH_STATUS = 0 + BEGIN + SET @stmt = 'GRANT ' + @perm_name + ' TO ' + QuoteName(@username) + EXEC (@stmt) + FETCH NEXT FROM grant_perm_cur INTO @perm_name + END + CLOSE grant_perm_cur + DEALLOCATE grant_perm_cur + + OPEN revoke_perm_cur + FETCH NEXT FROM revoke_perm_cur INTO @perm_name + WHILE @@FETCH_STATUS = 0 + BEGIN + SET @stmt = 'REVOKE ' + @perm_name + ' FROM ' + QuoteName(@username) + EXEC (@stmt) + FETCH NEXT FROM revoke_perm_cur INTO @perm_name + END + CLOSE revoke_perm_cur + DEALLOCATE revoke_perm_cur + ` + return c. + setDatabase(&permissions.DatabaseName). + ExecContext(ctx, cmd, + sql.Named("username", permissions.UserName), + sql.Named("permissions", strings.Join(permissions.Permissions, ",")), + ) +} + +func (c *Connector) DeleteDatabasePermissions(ctx context.Context, permissions *model.DatabasePermissions) error { + cmd := `DECLARE @stmt nvarchar(max) + DECLARE perm_cur CURSOR FOR SELECT value FROM String_Split(@permissions, ',') + DECLARE @permission_name nvarchar(max) + OPEN perm_cur + FETCH NEXT FROM perm_cur INTO @permission_name + WHILE @@FETCH_STATUS = 0 + BEGIN + SET @stmt = 'REVOKE ' + @permission_name + ' FROM ' + QuoteName(@username) + EXEC (@stmt) + FETCH NEXT FROM perm_cur INTO @permission_name + END + CLOSE perm_cur + DEALLOCATE perm_cur + ` + return c. + setDatabase(&permissions.DatabaseName). + ExecContext(ctx, cmd, + sql.Named("username", permissions.UserName), + sql.Named("permissions", strings.Join(permissions.Permissions, ",")), + ) +} \ No newline at end of file diff --git a/sql/database_role.go b/sql/database_role.go new file mode 100644 index 0000000..34f12d0 --- /dev/null +++ b/sql/database_role.go @@ -0,0 +1,117 @@ +package sql + +import ( + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetDatabaseRole(ctx context.Context, database, roleName string) (*model.DatabaseRole, error) { + cmd := `IF @@VERSION LIKE 'Microsoft SQL Azure%' AND @database = 'master' + BEGIN + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SELECT dp2.principal_id, dp2.name, dp2.owning_principal_id, '' AS ownerName FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp2.type = 'R' AND dp2.name = @roleName + END + ELSE + BEGIN + SELECT dp2.principal_id, dp2.name, dp2.owning_principal_id, dp1.name AS ownerName FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp2.type = 'R' AND dp2.name = @roleName + END + END + ELSE + BEGIN + SELECT dp2.principal_id, dp2.name, dp2.owning_principal_id, dp1.name AS ownerName FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp2.type = 'R' AND dp2.name = @roleName + END` + var role model.DatabaseRole + err := c. + setDatabase(&database). + QueryRowContext(ctx, cmd, + func(r *sql.Row) error { + return r.Scan(&role.RoleID, &role.RoleName, &role.OwnerId, &role.OwnerName) + }, + sql.Named("database", database), + sql.Named("roleName", roleName), + sql.Named("ownerName", role.OwnerName), + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &role, nil +} + +func (c *Connector) CreateDatabaseRole(ctx context.Context, database, roleName string, ownerName string) error { + cmd := `DECLARE @sql nvarchar(max); + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SET @sql = 'CREATE ROLE ' + QuoteName(@roleName) + END + ELSE + BEGIN + SET @sql = 'CREATE ROLE ' + QuoteName(@roleName) + ' AUTHORIZATION ' + QuoteName(@ownerName) + END + EXEC (@sql);` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("roleName", roleName), + sql.Named("ownerName", ownerName), + sql.Named("database", database), + ) +} + +func (c *Connector) DeleteDatabaseRole(ctx context.Context, database, roleName string) error { + cmd := `DECLARE @stmt nvarchar(max) + DECLARE @sql NVARCHAR(max) + DECLARE @user_name NVARCHAR(max) = (SELECT USER_NAME()) + DECLARE @roleNameowner NVARCHAR(max) = (SELECT dp2.name FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp1.name = @roleName) + SET @sql = 'IF EXISTS (SELECT 1 FROM ' + QuoteName(@database) + '.[sys].[database_principals] dp1 INNER JOIN ' + QuoteName(@database) + '.[sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp1.name = ' + QuoteName(@roleName, '''') + ') ' + + 'ALTER AUTHORIZATION ON ROLE:: [' + @roleNameowner + '] TO [' + @user_name + ']' + EXEC sp_executesql @sql; + SET @stmt = 'IF EXISTS (SELECT 1 FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE [name] = ' + QuoteName(@roleName, '''') + ') ' + + 'DROP ROLE ' + QuoteName(@roleName) + EXEC (@stmt)` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("database", database), + sql.Named("roleName", roleName), + ) +} + +func (c *Connector) UpdateDatabaseRole(ctx context.Context, database string, roleId int, roleName string, ownerName string) error { + cmd := `DECLARE @sql NVARCHAR(max) + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SET @ownerName = (SELECT USER_NAME()) + END + DECLARE @old_role_name NVARCHAR(max) = (SELECT name FROM [sys].[database_principals] WHERE [type] = 'R' AND [principal_id] = @principalId) + DECLARE @old_owner_name NVARCHAR(max) = (SELECT dp1.name AS ownerName FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp2.type = 'R' AND dp2.name = @roleName) + IF (@old_role_name != @roleName) AND (@old_owner_name = @ownerName) + BEGIN + SET @sql = 'ALTER ROLE ' + QuoteName(@old_role_name) + ' WITH NAME = ' + QuoteName(@roleName) + END + IF (@old_owner_name != @ownerName) AND (@old_role_name = @roleName) + BEGIN + SET @sql = 'ALTER AUTHORIZATION ON ROLE:: [' + @roleName + '] TO [' + @ownerName + ']' + END + ELSE + BEGIN + SET @sql = 'ALTER ROLE ' + QuoteName(@old_role_name) + ' WITH NAME = ' + QuoteName(@roleName) + ';' + SET @sql = @sql + 'ALTER AUTHORIZATION ON ROLE:: [' + @roleName + '] TO [' + @ownerName + '];' + END + EXEC (@sql)` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("roleName", roleName), + sql.Named("principalId", roleId), + sql.Named("ownerName", ownerName), + ) +} diff --git a/sql/database_schema.go b/sql/database_schema.go new file mode 100644 index 0000000..fb117ad --- /dev/null +++ b/sql/database_schema.go @@ -0,0 +1,109 @@ +package sql + +import ( + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" +) + +func (c *Connector) GetDatabaseSchema(ctx context.Context, database, schemaName string) (*model.DatabaseSchema, error) { + cmd := `IF @@VERSION LIKE 'Microsoft SQL Azure%' AND @database = 'master' + BEGIN + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SELECT dp1.schema_id, dp1.name, dp1.principal_id, '' AS name FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp1.name = @schemaName + END + ELSE + BEGIN + SELECT dp1.schema_id, dp1.name, dp1.principal_id, dp2.name FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp1.name = @schemaName + END + END + ELSE + BEGIN + SELECT dp1.schema_id, dp1.name, dp1.principal_id, dp2.name FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp1.name = @schemaName + END` + var sqlschema model.DatabaseSchema + err := c. + setDatabase(&database). + QueryRowContext(ctx, cmd, + func(r *sql.Row) error { + return r.Scan(&sqlschema.SchemaID, &sqlschema.SchemaName, &sqlschema.OwnerId, &sqlschema.OwnerName) + }, + sql.Named("database", database), + sql.Named("schemaName", schemaName), + sql.Named("ownerName", sqlschema.OwnerName), + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + return nil, err + } + return &sqlschema, nil +} + +func (c *Connector) CreateDatabaseSchema(ctx context.Context, database, schemaName string, ownerName string) error { + cmd := `DECLARE @sql nvarchar(max); + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SET @sql = 'CREATE SCHEMA ' + @schemaName + END + ELSE + BEGIN + SET @sql = 'CREATE SCHEMA ' + @schemaName + ' AUTHORIZATION ' + @ownerName + END + EXEC (@sql);` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("schemaName", schemaName), + sql.Named("ownerName", ownerName), + sql.Named("database", database), + ) +} + +func (c *Connector) DeleteDatabaseSchema(ctx context.Context, database, schemaName string) error { + cmd := `DECLARE @stmt nvarchar(max) + DECLARE @sql NVARCHAR(max) + DECLARE @user_name NVARCHAR(max) = (SELECT USER_NAME()) + IF @@VERSION LIKE 'Microsoft SQL Azure%' AND @database = 'master' + BEGIN + SET @stmt = 'IF EXISTS (SELECT 1 FROM [sys].[schemas] WHERE [name] = ' + QuoteName(@schemaName, '''') + ') ' + + 'DROP SCHEMA ' + @schemaName + END + ELSE + BEGIN + SET @sql = 'IF EXISTS (SELECT 1 FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp1.name = ' + QuoteName(@schemaName, '''') + ') ' + + 'ALTER AUTHORIZATION ON SCHEMA:: [' + @schemaName + '] TO [' + @user_name + ']' + EXEC sp_executesql @sql; + SET @stmt = 'IF EXISTS (SELECT 1 FROM [sys].[schemas] WHERE [name] = ' + QuoteName(@schemaName, '''') + ') ' + + 'DROP SCHEMA ' + @schemaName + END + EXEC (@stmt)` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("database", database), + sql.Named("schemaName", schemaName), + ) +} + +func (c *Connector) UpdateDatabaseSchema(ctx context.Context, database string, schemaName string, ownerName string) error { + cmd := `DECLARE @sql NVARCHAR(max) + IF @ownerName = 'dbo' OR @ownerName = '' + BEGIN + SET @ownerName = (SELECT USER_NAME()) + END + SET @sql = 'ALTER AUTHORIZATION ON SCHEMA:: [' + @schemaName + '] TO [' + @ownerName + ']' + EXEC (@sql)` + + return c. + setDatabase(&database). + ExecContext(ctx, cmd, + sql.Named("schemaName", schemaName), + sql.Named("ownerName", ownerName), + ) +} diff --git a/sql/login.go b/sql/login.go index 2a21d60..5c65553 100644 --- a/sql/login.go +++ b/sql/login.go @@ -1,15 +1,16 @@ package sql import ( - "context" - "database/sql" - "github.com/betr-io/terraform-provider-mssql/mssql/model" + "context" + "database/sql" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" ) func (c *Connector) GetLogin(ctx context.Context, name string) (*model.Login, error) { var login model.Login err := c.QueryRowContext(ctx, - "SELECT principal_id, name, CONVERT(VARCHAR(1000), [sid], 1), default_database_name, default_language_name FROM [master].[sys].[sql_logins] WHERE [name] = @name", + "SELECT principal_id, name, CONVERT(VARCHAR(85), [sid], 1), default_database_name, default_language_name FROM [master].[sys].[sql_logins] WHERE [name] = @name", func(r *sql.Row) error { return r.Scan(&login.PrincipalID, &login.LoginName, &login.SIDStr, &login.DefaultDatabase, &login.DefaultLanguage) }, @@ -30,7 +31,7 @@ func (c *Connector) CreateLogin(ctx context.Context, name, password, sid, defaul 'WITH PASSWORD = ' + QuoteName(@password, '''') IF NOT @sid = '' BEGIN - SET @sql = @sql + ', SID = ' + CONVERT(VARCHAR(1000), @sid, 1) + SET @sql = @sql + ', SID = ' + CONVERT(VARCHAR(85), @sid, 1) END IF @@VERSION NOT LIKE 'Microsoft SQL Azure%' BEGIN diff --git a/sql/user.go b/sql/user.go index 4c5c138..728c96b 100644 --- a/sql/user.go +++ b/sql/user.go @@ -1,10 +1,11 @@ package sql import ( - "context" - "database/sql" - "github.com/betr-io/terraform-provider-mssql/mssql/model" - "strings" + "context" + "database/sql" + "strings" + + "github.com/betr-io/terraform-provider-mssql/mssql/model" ) func (c *Connector) GetUser(ctx context.Context, database, username string) (*model.User, error) { @@ -18,7 +19,7 @@ func (c *Connector) GetUser(ctx context.Context, database, username string) (*mo ' SELECT member_principal_id, drm.role_principal_id FROM [sys].[database_role_members] drm' + ' INNER JOIN CTE_Roles cr ON drm.member_principal_id = cr.role_principal_id' + ') ' + - 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(1000), p.sid, 1) AS sidStr, '''', COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + + 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(85), p.sid, 1) AS sidStr, '''', COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + 'FROM [sys].[database_principals] p' + ' LEFT JOIN CTE_Roles r ON p.principal_id = r.principal_id ' + 'WHERE p.name = ' + QuoteName(@username, '''') + ' ' + @@ -33,7 +34,7 @@ func (c *Connector) GetUser(ctx context.Context, database, username string) (*mo ' SELECT member_principal_id, drm.role_principal_id FROM ' + QuoteName(@database) + '.[sys].[database_role_members] drm' + ' INNER JOIN CTE_Roles cr ON drm.member_principal_id = cr.role_principal_id' + ') ' + - 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(1000), p.sid, 1) AS sidStr, COALESCE(sl.name, ''''), COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + + 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(85), p.sid, 1) AS sidStr, COALESCE(sl.name, ''''), COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + 'FROM ' + QuoteName(@database) + '.[sys].[database_principals] p' + ' LEFT JOIN CTE_Roles r ON p.principal_id = r.principal_id ' + ' LEFT JOIN [master].[sys].[sql_logins] sl ON p.sid = sl.sid ' + @@ -106,12 +107,12 @@ func (c *Connector) CreateUser(ctx context.Context, database string, user *model BEGIN IF @objectId != '' BEGIN - SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' WITH SID=' + CONVERT(varchar(64), CAST(CAST(@objectId AS UNIQUEIDENTIFIER) AS VARBINARY(16)), 1) + ', TYPE=E' + SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' WITH DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) + ', SID = ' + CONVERT(varchar(64), CAST(CAST(@objectId AS UNIQUEIDENTIFIER) AS VARBINARY(16)), 1) + ', TYPE=E' END ELSE BEGIN - SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' FROM EXTERNAL PROVIDER' - END + SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' FROM EXTERNAL PROVIDER WITH DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) + END END ELSE BEGIN @@ -269,6 +270,42 @@ func (c *Connector) UpdateUser(ctx context.Context, database string, user *model func (c *Connector) DeleteUser(ctx context.Context, database, username string) error { cmd := `DECLARE @stmt nvarchar(max) + DECLARE @user_name NVARCHAR(max) = (SELECT USER_NAME()) + + IF EXISTS (SELECT 1 FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp1.name = @username) + BEGIN + DECLARE @role nvarchar(max) + DECLARE role_cur CURSOR FOR SELECT dp2.name FROM [sys].[database_principals] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.owning_principal_id AND dp1.name = @username + OPEN role_cur + FETCH NEXT FROM role_cur INTO @role + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @rolesql nvarchar(max) + SET @rolesql = 'ALTER AUTHORIZATION ON ROLE:: [' + @role + '] TO [' + @user_name + ']' + EXEC (@rolesql) + FETCH NEXT FROM role_cur INTO @role + END + CLOSE role_cur + DEALLOCATE role_cur + END + + IF EXISTS (SELECT 1 FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp2.name = @username) + BEGIN + DECLARE @schema nvarchar(max) + DECLARE schema_cur CURSOR FOR SELECT dp1.name FROM [sys].[schemas] dp1 INNER JOIN [sys].[database_principals] dp2 ON dp1.principal_id = dp2.principal_id AND dp2.name = @username + OPEN schema_cur + FETCH NEXT FROM schema_cur INTO @schema + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @schemasql nvarchar(max) + SET @schemasql = 'ALTER AUTHORIZATION ON SCHEMA:: [' + @schema + '] TO [' + @user_name + ']' + EXEC (@schemasql) + FETCH NEXT FROM schema_cur INTO @schema + END + CLOSE schema_cur + DEALLOCATE schema_cur + END + SET @stmt = 'IF EXISTS (SELECT 1 FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE [name] = ' + QuoteName(@username, '''') + ') ' + 'DROP USER ' + QuoteName(@username) EXEC (@stmt)`