Skip to content

Commit 6b7e4a6

Browse files
btiplingclaude
andcommitted
[BB-610] Add account provisioning capability for SQL Server users
This commit adds the ability to create SQL Server logins from Windows AD accounts: - Add CreateWindowsLogin method to create SQL Server logins from Windows authentication - Implement AccountManager interface in userPrincipalSyncer - Update baton_capabilities.json to include CREATE_ACCOUNT capability - Add account creation schema with domain and username fields - Update SDK dependencies 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent b2f1059 commit 6b7e4a6

File tree

17 files changed

+1085
-207
lines changed

17 files changed

+1085
-207
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@
1515
# Dependency directories (remove the comment below to include it)
1616
# vendor/
1717
dist/
18+
19+
**/.claude/settings.local.json

baton_capabilities.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@
6767
]
6868
},
6969
"capabilities": [
70-
"CAPABILITY_SYNC"
70+
"CAPABILITY_SYNC",
71+
"CAPABILITY_CREATE_ACCOUNT"
7172
]
7273
}
7374
],
7475
"connectorCapabilities": [
7576
"CAPABILITY_PROVISION",
76-
"CAPABILITY_SYNC"
77+
"CAPABILITY_SYNC",
78+
"CAPABILITY_CREATE_ACCOUNT"
7779
],
7880
"credentialDetails": {}
7981
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/conductorone/baton-sql-server
33
go 1.24.1
44

55
require (
6-
github.com/conductorone/baton-sdk v0.2.98
6+
github.com/conductorone/baton-sdk v0.2.99
77
github.com/ennyjfrick/ruleguard-logfatal v0.0.2
88
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
99
github.com/jmoiron/sqlx v1.3.5

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
6464
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
6565
github.com/conductorone/baton-sdk v0.2.98 h1:4kyfOPpujnZ3ZaNfxTY/LfFd41nHXBeek6APyyjSr4M=
6666
github.com/conductorone/baton-sdk v0.2.98/go.mod h1:nUgHSAf9P0lfamti5NlOSpeh1t99UNzMjIwf0I7n4/g=
67+
github.com/conductorone/baton-sdk v0.2.99 h1:klXBM3Qn8XmieDuV/ZVGa2k2ZlsrfK2gh5ygIsqrYsw=
68+
github.com/conductorone/baton-sdk v0.2.99/go.mod h1:nUgHSAf9P0lfamti5NlOSpeh1t99UNzMjIwf0I7n4/g=
6769
github.com/conductorone/dpop v0.2.3 h1:s91U3845GHQ6P6FWrdNr2SEOy1ES/jcFs1JtKSl2S+o=
6870
github.com/conductorone/dpop v0.2.3/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s=
6971
github.com/conductorone/dpop/integrations/dpop_grpc v0.2.3 h1:kLMCNIh0Mo2vbvvkCmJ3ixsPbXEJ6HPcW53Ku9yje3s=

pkg/connector/connector.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ func (o *Mssqldb) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
3535
DisplayName: fmt.Sprintf("Microsoft SQL Server (%s)", serverInfo.Name),
3636
Annotations: annos,
3737
Description: "Baton connector for Microsoft SQL Server connector",
38+
AccountCreationSchema: &v2.ConnectorAccountCreationSchema{
39+
FieldMap: map[string]*v2.ConnectorAccountCreationSchema_Field{
40+
"domain": {
41+
DisplayName: "Active Directory Domain",
42+
Required: false,
43+
Description: "The Active Directory domain for the user (optional). If provided, the login will be created as [DOMAIN\\Username].",
44+
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
45+
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
46+
},
47+
Placeholder: "DOMAIN",
48+
Order: 1,
49+
},
50+
"username": {
51+
DisplayName: "Username",
52+
Required: true,
53+
Description: "The Active Directory username for which to create a SQL Server login.",
54+
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
55+
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
56+
},
57+
Placeholder: "username",
58+
Order: 2,
59+
},
60+
},
61+
},
3862
}, nil
3963
}
4064

pkg/connector/server_user.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ package connector
22

33
import (
44
"context"
5+
"fmt"
56
"net/mail"
67

78
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
89
"github.com/conductorone/baton-sdk/pkg/annotations"
910
_ "github.com/conductorone/baton-sdk/pkg/annotations"
11+
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
1012
"github.com/conductorone/baton-sdk/pkg/pagination"
1113
enTypes "github.com/conductorone/baton-sdk/pkg/types/entitlement"
1214
"github.com/conductorone/baton-sdk/pkg/types/resource"
1315
"github.com/conductorone/baton-sql-server/pkg/mssqldb"
16+
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
17+
"go.uber.org/zap"
1418
)
1519

20+
// userPrincipalSyncer implements both ResourceSyncer and AccountManager.
1621
type userPrincipalSyncer struct {
1722
resourceType *v2.ResourceType
1823
client *mssqldb.Client
@@ -82,6 +87,130 @@ func (d *userPrincipalSyncer) Grants(ctx context.Context, resource *v2.Resource,
8287
return nil, "", nil, nil
8388
}
8489

90+
// CreateAccount creates a SQL Server login and database user for an Active Directory user.
91+
// It implements the AccountManager interface.
92+
func (d *userPrincipalSyncer) CreateAccount(
93+
ctx context.Context,
94+
accountInfo *v2.AccountInfo,
95+
credentialOptions *v2.CredentialOptions,
96+
) (connectorbuilder.CreateAccountResponse, []*v2.PlaintextData, annotations.Annotations, error) {
97+
l := ctxzap.Extract(ctx)
98+
99+
// Extract required username field from profile
100+
usernameVal := accountInfo.Profile.GetFields()["username"]
101+
if usernameVal == nil || usernameVal.GetStringValue() == "" {
102+
return nil, nil, nil, fmt.Errorf("missing required username field")
103+
}
104+
username := usernameVal.GetStringValue()
105+
106+
// Extract optional domain field from profile
107+
var domain string
108+
domainVal := accountInfo.Profile.GetFields()["domain"]
109+
if domainVal != nil && domainVal.GetStringValue() != "" {
110+
domain = domainVal.GetStringValue()
111+
}
112+
113+
// Create the Windows login
114+
err := d.client.CreateWindowsLogin(ctx, domain, username)
115+
if err != nil {
116+
l.Error("Failed to create Windows login", zap.Error(err))
117+
return nil, nil, nil, fmt.Errorf("failed to create Windows login: %w", err)
118+
}
119+
120+
// Determine the formatted username for the database user
121+
var formattedUsername string
122+
if domain != "" {
123+
formattedUsername = fmt.Sprintf("%s\\%s", domain, username)
124+
} else {
125+
formattedUsername = username
126+
}
127+
128+
// Get list of databases to create users in
129+
databases, _, err := d.client.ListDatabases(ctx, &mssqldb.Pager{})
130+
if err != nil {
131+
l.Error("Failed to retrieve databases", zap.Error(err))
132+
errMsg := fmt.Sprintf("Login created successfully, but failed to retrieve databases: %v", err)
133+
result := &v2.CreateAccountResponse_ActionRequiredResult{
134+
Message: errMsg,
135+
IsCreateAccountResult: true,
136+
}
137+
return result, nil, nil, nil
138+
}
139+
140+
// Create user in each database
141+
var dbsCreated []string
142+
for _, db := range databases {
143+
// Skip system databases
144+
if db.Name == "master" || db.Name == "tempdb" || db.Name == "model" || db.Name == "msdb" {
145+
continue
146+
}
147+
148+
err = d.client.CreateDatabaseUserForPrincipal(ctx, db.Name, formattedUsername)
149+
if err != nil {
150+
l.Error("Failed to create user in database",
151+
zap.String("database", db.Name),
152+
zap.String("user", formattedUsername),
153+
zap.Error(err))
154+
errMsg := fmt.Sprintf("Login created successfully, but failed to create user in some databases: %v", err)
155+
result := &v2.CreateAccountResponse_ActionRequiredResult{
156+
Message: errMsg,
157+
IsCreateAccountResult: true,
158+
}
159+
return result, nil, nil, nil
160+
}
161+
dbsCreated = append(dbsCreated, db.Name)
162+
}
163+
164+
// Create a resource for the newly created login
165+
profile := map[string]interface{}{
166+
"username": username,
167+
"domain": domain,
168+
"formatted_login": formattedUsername,
169+
"databases": dbsCreated,
170+
}
171+
172+
// Use email as name if it looks like an email address
173+
var userOpts []resource.UserTraitOption
174+
userOpts = append(userOpts, resource.WithUserProfile(profile))
175+
userOpts = append(userOpts, resource.WithStatus(v2.UserTrait_Status_STATUS_ENABLED))
176+
177+
if _, err = mail.ParseAddress(username); err == nil {
178+
userOpts = append(userOpts, resource.WithEmail(username, true))
179+
}
180+
181+
// Create a resource object to represent the user
182+
resource, err := resource.NewUserResource(
183+
formattedUsername,
184+
d.ResourceType(ctx),
185+
formattedUsername, // Use the formatted username as the ID
186+
userOpts,
187+
)
188+
if err != nil {
189+
l.Error("Failed to create resource for new user", zap.Error(err))
190+
return nil, nil, nil, fmt.Errorf("failed to create resource for new user: %w", err)
191+
}
192+
193+
// Return success result with the new user resource
194+
successResult := &v2.CreateAccountResponse_SuccessResult{
195+
Resource: resource,
196+
IsCreateAccountResult: true,
197+
}
198+
199+
return successResult, nil, nil, nil
200+
}
201+
202+
// CreateAccountCapabilityDetails returns the capability details for account creation.
203+
func (d *userPrincipalSyncer) CreateAccountCapabilityDetails(
204+
ctx context.Context,
205+
) (*v2.CredentialDetailsAccountProvisioning, annotations.Annotations, error) {
206+
return &v2.CredentialDetailsAccountProvisioning{
207+
SupportedCredentialOptions: []v2.CapabilityDetailCredentialOption{
208+
v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD,
209+
},
210+
PreferredCredentialOption: v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD,
211+
}, nil, nil
212+
}
213+
85214
func newUserPrincipalSyncer(ctx context.Context, c *mssqldb.Client) *userPrincipalSyncer {
86215
return &userPrincipalSyncer{
87216
resourceType: resourceTypeUser,

pkg/mssqldb/users.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,35 @@ CREATE USER [%s] FOR LOGIN [%s];
306306

307307
return nil
308308
}
309+
310+
// CreateWindowsLogin creates a SQL Server login from Windows AD for the specified domain and username.
311+
// If domain is provided, it will create the login in the format [DOMAIN\Username],
312+
// otherwise it will use just [Username].
313+
func (c *Client) CreateWindowsLogin(ctx context.Context, domain, username string) error {
314+
l := ctxzap.Extract(ctx)
315+
316+
// Check for invalid characters to prevent SQL injection
317+
if (domain != "" && strings.ContainsAny(domain, "[]\"';")) || strings.ContainsAny(username, "[]\"';") {
318+
return fmt.Errorf("invalid characters in domain or username")
319+
}
320+
321+
var loginName string
322+
if domain != "" {
323+
loginName = fmt.Sprintf("[%s\\%s]", domain, username)
324+
l.Debug("creating windows login with domain", zap.String("login", loginName))
325+
} else {
326+
loginName = fmt.Sprintf("[%s]", username)
327+
l.Debug("creating windows login without domain", zap.String("login", loginName))
328+
}
329+
330+
query := fmt.Sprintf("CREATE LOGIN %s FROM WINDOWS;", loginName)
331+
332+
l.Debug("SQL QUERY", zap.String("q", query))
333+
334+
_, err := c.db.ExecContext(ctx, query)
335+
if err != nil {
336+
return fmt.Errorf("failed to create Windows login: %w", err)
337+
}
338+
339+
return nil
340+
}

vendor/github.com/conductorone/baton-sdk/internal/connector/connector.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)