@@ -2,17 +2,24 @@ package connector
22
33import (
44 "context"
5+ "crypto/rand"
6+ "fmt"
7+ "math/big"
58 "net/mail"
69
710 v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
811 "github.com/conductorone/baton-sdk/pkg/annotations"
912 _ "github.com/conductorone/baton-sdk/pkg/annotations"
13+ "github.com/conductorone/baton-sdk/pkg/connectorbuilder"
1014 "github.com/conductorone/baton-sdk/pkg/pagination"
1115 enTypes "github.com/conductorone/baton-sdk/pkg/types/entitlement"
1216 "github.com/conductorone/baton-sdk/pkg/types/resource"
1317 "github.com/conductorone/baton-sql-server/pkg/mssqldb"
18+ "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
19+ "go.uber.org/zap"
1420)
1521
22+ // userPrincipalSyncer implements both ResourceSyncer and AccountManager.
1623type userPrincipalSyncer struct {
1724 resourceType * v2.ResourceType
1825 client * mssqldb.Client
@@ -82,6 +89,181 @@ func (d *userPrincipalSyncer) Grants(ctx context.Context, resource *v2.Resource,
8289 return nil , "" , nil , nil
8390}
8491
92+ // CreateAccount creates a SQL Server login based on the specified login type.
93+ // It implements the AccountManager interface.
94+ func (d * userPrincipalSyncer ) CreateAccount (
95+ ctx context.Context ,
96+ accountInfo * v2.AccountInfo ,
97+ credentialOptions * v2.CredentialOptions ,
98+ ) (connectorbuilder.CreateAccountResponse , []* v2.PlaintextData , annotations.Annotations , error ) {
99+ l := ctxzap .Extract (ctx )
100+
101+ // Extract required login_type field from profile
102+ loginTypeVal := accountInfo .Profile .GetFields ()["login_type" ]
103+ if loginTypeVal == nil || loginTypeVal .GetStringValue () == "" {
104+ return nil , nil , nil , fmt .Errorf ("missing required login_type field" )
105+ }
106+ loginTypeStr := loginTypeVal .GetStringValue ()
107+ loginType := mssqldb .LoginType (loginTypeStr )
108+
109+ // Extract required username field from profile
110+ usernameVal := accountInfo .Profile .GetFields ()["username" ]
111+ if usernameVal == nil || usernameVal .GetStringValue () == "" {
112+ return nil , nil , nil , fmt .Errorf ("missing required username field" )
113+ }
114+ username := usernameVal .GetStringValue ()
115+
116+ // Extract optional domain field (for Windows auth) or password (for SQL auth)
117+ var domain , password string
118+ var formattedUsername string
119+
120+ switch loginType {
121+ case mssqldb .LoginTypeWindows :
122+ // For Windows auth, extract domain
123+ domainVal := accountInfo .Profile .GetFields ()["domain" ]
124+ if domainVal != nil && domainVal .GetStringValue () != "" {
125+ domain = domainVal .GetStringValue ()
126+ }
127+
128+ if domain != "" {
129+ formattedUsername = fmt .Sprintf ("%s\\ %s" , domain , username )
130+ } else {
131+ formattedUsername = username
132+ }
133+ case mssqldb .LoginTypeSQL :
134+ // For SQL auth, generate a strong random password
135+ password = generateStrongPassword ()
136+ l .Debug ("generated random password for SQL Server authentication" )
137+ formattedUsername = username
138+ case mssqldb .LoginTypeAzureAD , mssqldb .LoginTypeEntraID :
139+ // For Azure AD or Entra ID, just use the username as is
140+ formattedUsername = username
141+ default :
142+ return nil , nil , nil , fmt .Errorf ("unsupported login type: %s" , loginType )
143+ }
144+
145+ // Create the login
146+ err := d .client .CreateLogin (ctx , loginType , domain , username , password )
147+ if err != nil {
148+ l .Error ("Failed to create login" , zap .Error (err ), zap .String ("loginType" , string (loginType )))
149+ return nil , nil , nil , fmt .Errorf ("failed to create login: %w" , err )
150+ }
151+
152+ // Create a resource for the newly created login
153+ profile := map [string ]interface {}{
154+ "username" : username ,
155+ "login_type" : string (loginType ),
156+ "formatted_login" : formattedUsername ,
157+ }
158+
159+ // Add domain if it exists (for Windows auth)
160+ if domain != "" {
161+ profile ["domain" ] = domain
162+ }
163+
164+ // Use email as name if it looks like an email address
165+ var userOpts []resource.UserTraitOption
166+ userOpts = append (userOpts , resource .WithUserProfile (profile ))
167+ userOpts = append (userOpts , resource .WithStatus (v2 .UserTrait_Status_STATUS_ENABLED ))
168+
169+ if _ , err = mail .ParseAddress (username ); err == nil {
170+ userOpts = append (userOpts , resource .WithEmail (username , true ))
171+ }
172+
173+ // Create a resource object to represent the user
174+ resource , err := resource .NewUserResource (
175+ formattedUsername ,
176+ d .ResourceType (ctx ),
177+ formattedUsername , // Use the formatted username as the ID
178+ userOpts ,
179+ )
180+ if err != nil {
181+ l .Error ("Failed to create resource for new user" , zap .Error (err ))
182+ return nil , nil , nil , fmt .Errorf ("failed to create resource for new user: %w" , err )
183+ }
184+
185+ // Prepare the response - for SQL auth, we need to return the generated password
186+ successResult := & v2.CreateAccountResponse_SuccessResult {
187+ Resource : resource ,
188+ IsCreateAccountResult : true ,
189+ }
190+
191+ var plaintextData []* v2.PlaintextData
192+ // If this is SQL authentication, return the generated password
193+ if loginType == mssqldb .LoginTypeSQL {
194+ plaintextData = []* v2.PlaintextData {
195+ {
196+ Name : "password" ,
197+ Description : "The generated password for SQL Server authentication" ,
198+ Schema : "text/plain" ,
199+ Bytes : []byte (password ),
200+ },
201+ }
202+ }
203+
204+ return successResult , plaintextData , nil , nil
205+ }
206+
207+ // CreateAccountCapabilityDetails returns the capability details for account creation.
208+ func (d * userPrincipalSyncer ) CreateAccountCapabilityDetails (
209+ ctx context.Context ,
210+ ) (* v2.CredentialDetailsAccountProvisioning , annotations.Annotations , error ) {
211+ return & v2.CredentialDetailsAccountProvisioning {
212+ SupportedCredentialOptions : []v2.CapabilityDetailCredentialOption {
213+ v2 .CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD , // For Windows/Azure AD/Entra ID
214+ v2 .CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_RANDOM_PASSWORD , // For SQL Server auth
215+ },
216+ PreferredCredentialOption : v2 .CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD ,
217+ }, nil , nil
218+ }
219+
220+ // generateStrongPassword creates a secure random password for SQL Server.
221+ // The password meets SQL Server complexity requirements:
222+ // - At least 8 characters in length
223+ // - Contains uppercase, lowercase, numbers, and special characters.
224+ func generateStrongPassword () string {
225+ const (
226+ uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
227+ lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
228+ numberChars = "0123456789"
229+ specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
230+ passwordLength = 16
231+ )
232+
233+ // Ensure at least one character from each category
234+ password := make ([]byte , passwordLength )
235+
236+ // Add at least one character from each required group
237+ addRandomChar := func (charSet string , position int ) {
238+ maxVal := big .NewInt (int64 (len (charSet )))
239+ randomIndex , _ := rand .Int (rand .Reader , maxVal )
240+ password [position ] = charSet [randomIndex .Int64 ()]
241+ }
242+
243+ // Add one of each required character type
244+ addRandomChar (uppercaseChars , 0 )
245+ addRandomChar (lowercaseChars , 1 )
246+ addRandomChar (numberChars , 2 )
247+ addRandomChar (specialChars , 3 )
248+
249+ // Fill the rest with random characters from all sets
250+ allChars := uppercaseChars + lowercaseChars + numberChars + specialChars
251+ for i := 4 ; i < passwordLength ; i ++ {
252+ maxVal := big .NewInt (int64 (len (allChars )))
253+ randomIndex , _ := rand .Int (rand .Reader , maxVal )
254+ password [i ] = allChars [randomIndex .Int64 ()]
255+ }
256+
257+ // Shuffle the password to avoid predictable positions of character types
258+ for i := passwordLength - 1 ; i > 0 ; i -- {
259+ maxVal := big .NewInt (int64 (i + 1 ))
260+ j , _ := rand .Int (rand .Reader , maxVal )
261+ password [i ], password [j .Int64 ()] = password [j .Int64 ()], password [i ]
262+ }
263+
264+ return string (password )
265+ }
266+
85267func newUserPrincipalSyncer (ctx context.Context , c * mssqldb.Client ) * userPrincipalSyncer {
86268 return & userPrincipalSyncer {
87269 resourceType : resourceTypeUser ,
0 commit comments