Skip to content

Commit af1385a

Browse files
authored
Merge pull request #23 from ConductorOne/BB-610-add-account-provisioning
[BB-610] Add account provisioning capability for SQL Server users
2 parents b2f1059 + 6fb1785 commit af1385a

File tree

19 files changed

+1210
-216
lines changed

19 files changed

+1210
-216
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

.gon-amd64.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"source": ["./dist/macos-amd64_darwin_amd64_v1/baton-sql-server"],
33
"bundle_id": "com.conductorone.baton-sql-server",
44
"apple_id": {
5-
"username" : "[email protected]"
5+
"username": "[email protected]"
66
},
77
"sign": {
88
"application_identity": "Developer ID Application: Justin Gallardo (858DKH55XL)"
99
},
10-
"zip" :{
11-
"output_path": "./dist/baton-sql-server-darwin-amd64.signed.zip"
10+
"zip": {
11+
"output_path": "./dist/baton-sql-server-darwin-amd64.signed.zip"
1212
}
13-
}
13+
}

.gon-arm64.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
2-
"source": ["./dist/macos-arm64_darwin_arm64/baton-sql-server"],
2+
"source": ["./dist/macos-arm64_darwin_arm64_v8.0/baton-sql-server"],
33
"bundle_id": "com.conductorone.baton-sql-server",
44
"apple_id": {
5-
"username" : "[email protected]"
5+
"username": "[email protected]"
66
},
77
"sign": {
88
"application_identity": "Developer ID Application: Justin Gallardo (858DKH55XL)"
99
},
10-
"zip" :{
11-
"output_path": "./dist/baton-sql-server-darwin-arm64.signed.zip"
10+
"zip": {
11+
"output_path": "./dist/baton-sql-server-darwin-arm64.signed.zip"
1212
}
13-
}
13+
}

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,40 @@ 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+
"login_type": {
41+
DisplayName: "Login Type",
42+
Required: true,
43+
Description: "The type of SQL Server authentication to use (WINDOWS, SQL, AZURE_AD, or ENTRA_ID).",
44+
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
45+
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
46+
},
47+
Placeholder: "WINDOWS",
48+
Order: 1,
49+
},
50+
"domain": {
51+
DisplayName: "Active Directory Domain",
52+
Required: false,
53+
Description: "The Active Directory domain for the user. Only used for Windows Authentication.",
54+
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
55+
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
56+
},
57+
Placeholder: "DOMAIN",
58+
Order: 2,
59+
},
60+
"username": {
61+
DisplayName: "Username",
62+
Required: true,
63+
Description: "The username for which to create a SQL Server login.",
64+
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
65+
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
66+
},
67+
Placeholder: "username",
68+
Order: 3,
69+
},
70+
},
71+
},
3872
}, nil
3973
}
4074

pkg/connector/server_user.go

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

33
import (
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.
1623
type 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+
85267
func newUserPrincipalSyncer(ctx context.Context, c *mssqldb.Client) *userPrincipalSyncer {
86268
return &userPrincipalSyncer{
87269
resourceType: resourceTypeUser,

0 commit comments

Comments
 (0)