diff --git a/docs/resources/login.md b/docs/resources/login.md index e079f61..b2a2a39 100644 --- a/docs/resources/login.md +++ b/docs/resources/login.md @@ -21,6 +21,7 @@ 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. Changing this forces a new resource to be created. * `password` - (Required) The password of the server login. +* `sid` - (Optional) The security identifier (SID). * `default_database` - (Optional) The default database of this server login. Defaults to `master`. This argument does not apply to Azure SQL Database. * `default_language` - (Optional) The default language of this server login. Defaults to `us_english`. This argument does not apply to Azure SQL Database. @@ -55,6 +56,7 @@ The `azuread_managed_identity_auth` block supports the following arguments: The following attributes are exported: * `principal_id` - The principal id of this server login. +* `sid` - The security identifier (SID) of this login in String format. ## Import diff --git a/examples/local/main.tf b/examples/local/main.tf index 7ea8c9e..2caae39 100644 --- a/examples/local/main.tf +++ b/examples/local/main.tf @@ -79,6 +79,21 @@ resource "mssql_login" "example" { depends_on = [time_sleep.wait_5_seconds] } +resource "mssql_login" "example" { + server { + host = docker_container.mssql.ip_address + login { + username = local.local_username + password = local.local_password + } + } + login_name = random_password.example.keepers.login_name + password = random_password.example.result + sid = "0xB7BDEF7990D03541BAA2AD73E4FF18E8" + + depends_on = [time_sleep.wait_5_seconds] +} + resource "mssql_user" "example" { server { host = docker_container.mssql.network_data[0].ip_address diff --git a/mssql/model/login.go b/mssql/model/login.go index a939ed4..7c4fabb 100644 --- a/mssql/model/login.go +++ b/mssql/model/login.go @@ -3,6 +3,7 @@ package model type Login struct { PrincipalID int64 LoginName string + SIDStr string DefaultDatabase string DefaultLanguage string } diff --git a/mssql/resource_login.go b/mssql/resource_login.go index fe2e682..c577dcd 100644 --- a/mssql/resource_login.go +++ b/mssql/resource_login.go @@ -15,7 +15,7 @@ const defaultDatabaseDefault = "master" const defaultLanguageProp = "default_language" type LoginConnector interface { - CreateLogin(ctx context.Context, name, password, defaultDatabase, defaultLanguage string) error + CreateLogin(ctx context.Context, name, password, sid, defaultDatabase, defaultLanguage string) error GetLogin(ctx context.Context, name string) (*model.Login, error) UpdateLogin(ctx context.Context, name, password, defaultDatabase, defaultLanguage string) error DeleteLogin(ctx context.Context, name string) error @@ -49,6 +49,12 @@ func resourceLogin() *schema.Resource { Required: true, Sensitive: true, }, + sidStrProp: { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, defaultDatabaseProp: { Type: schema.TypeString, Optional: true, @@ -81,6 +87,7 @@ func resourceLoginCreate(ctx context.Context, data *schema.ResourceData, meta in loginName := data.Get(loginNameProp).(string) password := data.Get(passwordProp).(string) + sid := data.Get(sidStrProp).(string) defaultDatabase := data.Get(defaultDatabaseProp).(string) defaultLanguage := data.Get(defaultLanguageProp).(string) @@ -89,7 +96,7 @@ func resourceLoginCreate(ctx context.Context, data *schema.ResourceData, meta in return diag.FromErr(err) } - if err = connector.CreateLogin(ctx, loginName, password, defaultDatabase, defaultLanguage); err != nil { + if err = connector.CreateLogin(ctx, loginName, password, sid, defaultDatabase, defaultLanguage); err != nil { return diag.FromErr(errors.Wrapf(err, "unable to create login [%s]", loginName)) } @@ -122,6 +129,9 @@ func resourceLoginRead(ctx context.Context, data *schema.ResourceData, meta inte 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) } @@ -220,6 +230,9 @@ func resourceLoginImport(ctx context.Context, data *schema.ResourceData, meta in if err = data.Set(principalIdProp, login.PrincipalID); err != nil { return nil, err } + if err = data.Set(sidStrProp, login.SIDStr); err != nil { + return nil, err + } if err = data.Set(defaultDatabaseProp, login.DefaultDatabase); err != nil { return nil, err } diff --git a/mssql/resource_login_test.go b/mssql/resource_login_test.go index 615c79c..7c067c4 100644 --- a/mssql/resource_login_test.go +++ b/mssql/resource_login_test.go @@ -38,6 +38,37 @@ func TestAccLogin_Local_Basic(t *testing.T) { }) } +func TestAccLogin_Local_Basic_SID(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IsUnitTest: runLocalAccTests, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckLogin(t, "basic", false, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡", "sid": "0xB7BDEF7990D03541BAA2AD73E4FF18E8"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoginExists("mssql_login.basic"), + testAccCheckLoginWorks("mssql_login.basic"), + resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), + resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), + resource.TestCheckResourceAttr("mssql_login.basic", "sid", "0xB7BDEF7990D03541BAA2AD73E4FF18E8"), + resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), + resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", "localhost"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "1"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), + ), + }, + }, + }) +} + func TestAccLogin_Azure_Basic(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -67,6 +98,36 @@ func TestAccLogin_Azure_Basic(t *testing.T) { }) } +func TestAccLogin_Azure_Basic_SID(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, + Steps: []resource.TestStep{ + { + Config: testAccCheckLogin(t, "basic", true, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡", "sid": "0x01060000000000640000000000000000BAF5FC800B97EF49AC6FD89469C4987F"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckLoginExists("mssql_login.basic"), + resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), + resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), + resource.TestCheckResourceAttr("mssql_login.basic", "sid", "0x01060000000000640000000000000000BAF5FC800B97EF49AC6FD89469C4987F"), + resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), + resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "1"), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), + resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "0"), + resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), + ), + }, + }, + }) +} + func TestAccLogin_Local_UpdateLoginName(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -225,6 +286,7 @@ func testAccCheckLogin(t *testing.T, name string, azure bool, data map[string]in } login_name = "{{ .login_name }}" password = "{{ .password }}" + {{ with .sid }}sid = "{{ . }}"{{ end }} {{ with .default_database }}default_database = "{{ . }}"{{ end }} {{ with .default_language }}default_language = "{{ . }}"{{ end }} }` diff --git a/sql/login.go b/sql/login.go index b88b82c..2a21d60 100644 --- a/sql/login.go +++ b/sql/login.go @@ -9,9 +9,9 @@ import ( func (c *Connector) GetLogin(ctx context.Context, name string) (*model.Login, error) { var login model.Login err := c.QueryRowContext(ctx, - "SELECT principal_id, name, default_database_name, default_language_name FROM [master].[sys].[sql_logins] WHERE [name] = @name", + "SELECT principal_id, name, CONVERT(VARCHAR(1000), [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.DefaultDatabase, &login.DefaultLanguage) + return r.Scan(&login.PrincipalID, &login.LoginName, &login.SIDStr, &login.DefaultDatabase, &login.DefaultLanguage) }, sql.Named("name", name), ) @@ -24,10 +24,14 @@ func (c *Connector) GetLogin(ctx context.Context, name string) (*model.Login, er return &login, nil } -func (c *Connector) CreateLogin(ctx context.Context, name, password, defaultDatabase, defaultLanguage string) error { +func (c *Connector) CreateLogin(ctx context.Context, name, password, sid, defaultDatabase, defaultLanguage string) error { cmd := `DECLARE @sql nvarchar(max) SET @sql = 'CREATE LOGIN ' + QuoteName(@name) + ' ' + 'WITH PASSWORD = ' + QuoteName(@password, '''') + IF NOT @sid = '' + BEGIN + SET @sql = @sql + ', SID = ' + CONVERT(VARCHAR(1000), @sid, 1) + END IF @@VERSION NOT LIKE 'Microsoft SQL Azure%' BEGIN IF @defaultDatabase = '' SET @defaultDatabase = 'master' @@ -48,6 +52,7 @@ func (c *Connector) CreateLogin(ctx context.Context, name, password, defaultData ExecContext(ctx, cmd, sql.Named("name", name), sql.Named("password", password), + sql.Named("sid", sid), sql.Named("defaultDatabase", defaultDatabase), sql.Named("defaultLanguage", defaultLanguage)) }