Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/
dist/

**/.claude/settings.local.json
8 changes: 4 additions & 4 deletions .gon-amd64.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"source": ["./dist/macos-amd64_darwin_amd64_v1/baton-sql-server"],
"bundle_id": "com.conductorone.baton-sql-server",
"apple_id": {
"username" : "justin.gallardo@conductorone.com"
"username": "justin.gallardo@conductorone.com"
},
"sign": {
"application_identity": "Developer ID Application: Justin Gallardo (858DKH55XL)"
},
"zip" :{
"output_path": "./dist/baton-sql-server-darwin-amd64.signed.zip"
"zip": {
"output_path": "./dist/baton-sql-server-darwin-amd64.signed.zip"
}
}
}
10 changes: 5 additions & 5 deletions .gon-arm64.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"source": ["./dist/macos-arm64_darwin_arm64/baton-sql-server"],
"source": ["./dist/macos-arm64_darwin_arm64_v8.0/baton-sql-server"],
"bundle_id": "com.conductorone.baton-sql-server",
"apple_id": {
"username" : "justin.gallardo@conductorone.com"
"username": "justin.gallardo@conductorone.com"
},
"sign": {
"application_identity": "Developer ID Application: Justin Gallardo (858DKH55XL)"
},
"zip" :{
"output_path": "./dist/baton-sql-server-darwin-arm64.signed.zip"
"zip": {
"output_path": "./dist/baton-sql-server-darwin-arm64.signed.zip"
}
}
}
6 changes: 4 additions & 2 deletions baton_capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@
]
},
"capabilities": [
"CAPABILITY_SYNC"
"CAPABILITY_SYNC",
"CAPABILITY_CREATE_ACCOUNT"
]
}
],
"connectorCapabilities": [
"CAPABILITY_PROVISION",
"CAPABILITY_SYNC"
"CAPABILITY_SYNC",
"CAPABILITY_CREATE_ACCOUNT"
],
"credentialDetails": {}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/conductorone/baton-sql-server
go 1.24.1

require (
github.com/conductorone/baton-sdk v0.2.98
github.com/conductorone/baton-sdk v0.2.99
github.com/ennyjfrick/ruleguard-logfatal v0.0.2
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/jmoiron/sqlx v1.3.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/conductorone/baton-sdk v0.2.98 h1:4kyfOPpujnZ3ZaNfxTY/LfFd41nHXBeek6APyyjSr4M=
github.com/conductorone/baton-sdk v0.2.98/go.mod h1:nUgHSAf9P0lfamti5NlOSpeh1t99UNzMjIwf0I7n4/g=
github.com/conductorone/baton-sdk v0.2.99 h1:klXBM3Qn8XmieDuV/ZVGa2k2ZlsrfK2gh5ygIsqrYsw=
github.com/conductorone/baton-sdk v0.2.99/go.mod h1:nUgHSAf9P0lfamti5NlOSpeh1t99UNzMjIwf0I7n4/g=
github.com/conductorone/dpop v0.2.3 h1:s91U3845GHQ6P6FWrdNr2SEOy1ES/jcFs1JtKSl2S+o=
github.com/conductorone/dpop v0.2.3/go.mod h1:gyo8TtzB9SCFCsjsICH4IaLZ7y64CcrDXMOPBwfq/3s=
github.com/conductorone/dpop/integrations/dpop_grpc v0.2.3 h1:kLMCNIh0Mo2vbvvkCmJ3ixsPbXEJ6HPcW53Ku9yje3s=
Expand Down
34 changes: 34 additions & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,40 @@ func (o *Mssqldb) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
DisplayName: fmt.Sprintf("Microsoft SQL Server (%s)", serverInfo.Name),
Annotations: annos,
Description: "Baton connector for Microsoft SQL Server connector",
AccountCreationSchema: &v2.ConnectorAccountCreationSchema{
FieldMap: map[string]*v2.ConnectorAccountCreationSchema_Field{
"login_type": {
DisplayName: "Login Type",
Required: true,
Description: "The type of SQL Server authentication to use (WINDOWS, SQL, AZURE_AD, or ENTRA_ID).",
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
},
Placeholder: "WINDOWS",
Order: 1,
},
"domain": {
DisplayName: "Active Directory Domain",
Required: false,
Description: "The Active Directory domain for the user. Only used for Windows Authentication.",
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
},
Placeholder: "DOMAIN",
Order: 2,
},
"username": {
DisplayName: "Username",
Required: true,
Description: "The username for which to create a SQL Server login.",
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
},
Placeholder: "username",
Order: 3,
},
},
},
}, nil
}

Expand Down
182 changes: 182 additions & 0 deletions pkg/connector/server_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package connector

import (
"context"
"crypto/rand"
"fmt"
"math/big"
"net/mail"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
_ "github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
"github.com/conductorone/baton-sdk/pkg/pagination"
enTypes "github.com/conductorone/baton-sdk/pkg/types/entitlement"
"github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/conductorone/baton-sql-server/pkg/mssqldb"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
)

// userPrincipalSyncer implements both ResourceSyncer and AccountManager.
type userPrincipalSyncer struct {
resourceType *v2.ResourceType
client *mssqldb.Client
Expand Down Expand Up @@ -82,6 +89,181 @@ func (d *userPrincipalSyncer) Grants(ctx context.Context, resource *v2.Resource,
return nil, "", nil, nil
}

// CreateAccount creates a SQL Server login based on the specified login type.
// It implements the AccountManager interface.
func (d *userPrincipalSyncer) CreateAccount(
ctx context.Context,
accountInfo *v2.AccountInfo,
credentialOptions *v2.CredentialOptions,
) (connectorbuilder.CreateAccountResponse, []*v2.PlaintextData, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)

// Extract required login_type field from profile
loginTypeVal := accountInfo.Profile.GetFields()["login_type"]
if loginTypeVal == nil || loginTypeVal.GetStringValue() == "" {
return nil, nil, nil, fmt.Errorf("missing required login_type field")
}
loginTypeStr := loginTypeVal.GetStringValue()
loginType := mssqldb.LoginType(loginTypeStr)

// Extract required username field from profile
usernameVal := accountInfo.Profile.GetFields()["username"]
if usernameVal == nil || usernameVal.GetStringValue() == "" {
return nil, nil, nil, fmt.Errorf("missing required username field")
}
username := usernameVal.GetStringValue()

// Extract optional domain field (for Windows auth) or password (for SQL auth)
var domain, password string
var formattedUsername string

switch loginType {
case mssqldb.LoginTypeWindows:
// For Windows auth, extract domain
domainVal := accountInfo.Profile.GetFields()["domain"]
if domainVal != nil && domainVal.GetStringValue() != "" {
domain = domainVal.GetStringValue()
}

if domain != "" {
formattedUsername = fmt.Sprintf("%s\\%s", domain, username)
} else {
formattedUsername = username
}
case mssqldb.LoginTypeSQL:
// For SQL auth, generate a strong random password
password = generateStrongPassword()
l.Debug("generated random password for SQL Server authentication")
formattedUsername = username
case mssqldb.LoginTypeAzureAD, mssqldb.LoginTypeEntraID:
// For Azure AD or Entra ID, just use the username as is
formattedUsername = username
default:
return nil, nil, nil, fmt.Errorf("unsupported login type: %s", loginType)
}

// Create the login
err := d.client.CreateLogin(ctx, loginType, domain, username, password)
if err != nil {
l.Error("Failed to create login", zap.Error(err), zap.String("loginType", string(loginType)))
return nil, nil, nil, fmt.Errorf("failed to create login: %w", err)
}

// Create a resource for the newly created login
profile := map[string]interface{}{
"username": username,
"login_type": string(loginType),
"formatted_login": formattedUsername,
}

// Add domain if it exists (for Windows auth)
if domain != "" {
profile["domain"] = domain
}

// Use email as name if it looks like an email address
var userOpts []resource.UserTraitOption
userOpts = append(userOpts, resource.WithUserProfile(profile))
userOpts = append(userOpts, resource.WithStatus(v2.UserTrait_Status_STATUS_ENABLED))

if _, err = mail.ParseAddress(username); err == nil {
userOpts = append(userOpts, resource.WithEmail(username, true))
}

// Create a resource object to represent the user
resource, err := resource.NewUserResource(
formattedUsername,
d.ResourceType(ctx),
formattedUsername, // Use the formatted username as the ID
userOpts,
)
if err != nil {
l.Error("Failed to create resource for new user", zap.Error(err))
return nil, nil, nil, fmt.Errorf("failed to create resource for new user: %w", err)
}

// Prepare the response - for SQL auth, we need to return the generated password
successResult := &v2.CreateAccountResponse_SuccessResult{
Resource: resource,
IsCreateAccountResult: true,
}

var plaintextData []*v2.PlaintextData
// If this is SQL authentication, return the generated password
if loginType == mssqldb.LoginTypeSQL {
plaintextData = []*v2.PlaintextData{
{
Name: "password",
Description: "The generated password for SQL Server authentication",
Schema: "text/plain",
Bytes: []byte(password),
},
}
}

return successResult, plaintextData, nil, nil
}

// CreateAccountCapabilityDetails returns the capability details for account creation.
func (d *userPrincipalSyncer) CreateAccountCapabilityDetails(
ctx context.Context,
) (*v2.CredentialDetailsAccountProvisioning, annotations.Annotations, error) {
return &v2.CredentialDetailsAccountProvisioning{
SupportedCredentialOptions: []v2.CapabilityDetailCredentialOption{
v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD, // For Windows/Azure AD/Entra ID
v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_RANDOM_PASSWORD, // For SQL Server auth
},
PreferredCredentialOption: v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD,
}, nil, nil
}

// generateStrongPassword creates a secure random password for SQL Server.
// The password meets SQL Server complexity requirements:
// - At least 8 characters in length
// - Contains uppercase, lowercase, numbers, and special characters.
func generateStrongPassword() string {
const (
uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowercaseChars = "abcdefghijklmnopqrstuvwxyz"
numberChars = "0123456789"
specialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?"
passwordLength = 16
)

// Ensure at least one character from each category
password := make([]byte, passwordLength)

// Add at least one character from each required group
addRandomChar := func(charSet string, position int) {
maxVal := big.NewInt(int64(len(charSet)))
randomIndex, _ := rand.Int(rand.Reader, maxVal)
password[position] = charSet[randomIndex.Int64()]
}

// Add one of each required character type
addRandomChar(uppercaseChars, 0)
addRandomChar(lowercaseChars, 1)
addRandomChar(numberChars, 2)
addRandomChar(specialChars, 3)

// Fill the rest with random characters from all sets
allChars := uppercaseChars + lowercaseChars + numberChars + specialChars
for i := 4; i < passwordLength; i++ {
maxVal := big.NewInt(int64(len(allChars)))
randomIndex, _ := rand.Int(rand.Reader, maxVal)
password[i] = allChars[randomIndex.Int64()]
}

// Shuffle the password to avoid predictable positions of character types
for i := passwordLength - 1; i > 0; i-- {
maxVal := big.NewInt(int64(i + 1))
j, _ := rand.Int(rand.Reader, maxVal)
password[i], password[j.Int64()] = password[j.Int64()], password[i]
}

return string(password)
}

func newUserPrincipalSyncer(ctx context.Context, c *mssqldb.Client) *userPrincipalSyncer {
return &userPrincipalSyncer{
resourceType: resourceTypeUser,
Expand Down
Loading
Loading