internal/accounts/accounts_windows.go (159 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package accounts
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"math/big"
mathRand "math/rand"
"os/user"
"syscall"
)
// windowsUserInfo contains windows specific user information.
type windowsUserInfo struct {
// UserInfo1 is the Windows UserInfo1 representation of a user.
UserInfo1 *UserInfo1
// SID is the user's SID, looked up from the user.User Uid.
SID *syscall.SID
}
var (
// AdminGroup is the administrator group.
AdminGroup = &Group{Name: "Administrators"}
// The following has been stubbed out for error injection testing.
lookupUser = user.Lookup
lookupGroup = user.LookupGroup
netUserAdd = defaultNetUserAdd
netUserDel = defaultNetUserDel
netUserGetInfo = defaultNetUserGetInfo
netUserSetPassword = defaultNetUserSetPassword
netLocalGroupAdd = defaultNetLocalGroupAdd
netLocalGroupDel = defaultNetLocalGroupDel
netLocalGroupAddMembers = defaultNetLocalGroupAddMembers
netLocalGroupDelMembers = defaultNetLocalGroupDelMembers
)
// SetPassword sets the password for the current user.
func (u *User) SetPassword(_ context.Context, password string) error {
return netUserSetPassword(u.Name, password)
}
// FindUser returns the user with the given username. If the user does not
// exist, it returns an error.
func FindUser(_ context.Context, username string) (*User, error) {
user, err := lookupUser(username)
if err != nil {
return nil, fmt.Errorf("failed to find user: %w", err)
}
// Get the user's info.
userInfo, err := netUserGetInfo(username)
if err != nil {
return nil, fmt.Errorf("failed to get user info: %w", err)
}
// Parse the user's SID.
sid, err := syscall.StringToSid(user.Uid)
if err != nil {
return nil, fmt.Errorf("failed to parse user sid: %w", err)
}
// Create the user info struct and return it.
// Passwords are not returned by the syscall for security reasons.
osSpecific := &windowsUserInfo{
UserInfo1: userInfo,
SID: sid,
}
return &User{
Username: user.Name,
Name: user.Name,
HomeDir: user.HomeDir,
UID: user.Uid,
GID: user.Gid,
osSpecific: osSpecific,
}, nil
}
// CreateUser creates a new user with the given user info.
func CreateUser(_ context.Context, u *User) error {
if u == nil {
return fmt.Errorf("user is nil")
}
// Create a new user using the password.
if err := netUserAdd(u.Name, u.Password); err != nil {
return fmt.Errorf("failed to add user: %w", err)
}
u.Password = ""
return nil
}
// DelUser deletes the user from the system.
func DelUser(_ context.Context, u *User) error {
if err := netUserDel(u.Name); err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
// CreateGroup creates a new group with the given name.
func CreateGroup(_ context.Context, group string) error {
if err := netLocalGroupAdd(group); err != nil {
return fmt.Errorf("failed to create group: %w", err)
}
return nil
}
// DelGroup deletes the group from the system.
func DelGroup(_ context.Context, g *Group) error {
if err := netLocalGroupDel(g.Name); err != nil {
return fmt.Errorf("failed to delete group: %w", err)
}
return nil
}
// FindGroup returns the group with the given name. If the group does not exist,
// it returns an error.
func FindGroup(_ context.Context, name string) (*Group, error) {
groupInfo, err := lookupGroup(name)
if err != nil {
return nil, fmt.Errorf("failed to find group: %w", err)
}
return &Group{Name: groupInfo.Name, GID: groupInfo.Gid}, nil
}
// AddUserToGroup adds the user to the given group.
func AddUserToGroup(_ context.Context, u *User, g *Group) error {
osSpecific, ok := u.osSpecific.(*windowsUserInfo)
if !ok {
return fmt.Errorf("failed to get os specific user info for user")
}
if err := netLocalGroupAddMembers(osSpecific.SID, g.Name); err != nil {
return fmt.Errorf("failed to add user %s to group %v: %w", u.Username, g.Name, err)
}
return nil
}
// RemoveUserFromGroup removes the provided user from the given group. If the
// user is not a member of the group, this is a no-op.
func RemoveUserFromGroup(_ context.Context, u *User, g *Group) error {
osSpecific, ok := u.osSpecific.(*windowsUserInfo)
if !ok {
return fmt.Errorf("failed to get os specific user info for user")
}
if err := netLocalGroupDelMembers(osSpecific.SID, g.Name); err != nil {
return fmt.Errorf("failed to remove user %s from group %v: %w", u.Username, g.Name, err)
}
return nil
}
// GeneratePassword will generate a random password that meets Windows
// complexity requirements:
//
// https://technet.microsoft.com/en-us/library/cc786468.
//
// Characters that are difficult for users to type on a command line (quotes,
// non english characters) are not used.
func GeneratePassword(userPwLgth int) (string, error) {
var pwLgth int
minPwLgth := 15
maxPwLgth := 255
lower := []byte("abcdefghijklmnopqrstuvwxyz")
upper := []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
numbers := []byte("0123456789")
special := []byte(`~!@#$%^&*_-+=|\(){}[]:;<>,.?/`)
chars := bytes.Join([][]byte{lower, upper, numbers, special}, nil)
pwLgth = minPwLgth
if userPwLgth > minPwLgth {
pwLgth = userPwLgth
}
if userPwLgth > maxPwLgth {
pwLgth = maxPwLgth
}
var pwd []byte
// Get one of each character type.
// Get a lowercase letter.
lowerIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(lower))))
if err != nil {
return "", fmt.Errorf("failed to generate password: %v", err)
}
pwd = append(pwd, lower[lowerIndex.Int64()])
// Get an uppercase letter.
upperIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(upper))))
if err != nil {
return "", fmt.Errorf("failed to generate password: %v", err)
}
pwd = append(pwd, upper[upperIndex.Int64()])
// Get a number.
numbersIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(numbers))))
if err != nil {
return "", fmt.Errorf("failed to generate password: %v", err)
}
pwd = append(pwd, numbers[numbersIndex.Int64()])
// Get a special character.
specialIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(special))))
if err != nil {
return "", fmt.Errorf("failed to generate password: %v", err)
}
pwd = append(pwd, special[specialIndex.Int64()])
// Fill the rest with random characters.
for i := 0; i < pwLgth-4; i++ {
// Get a random character.
randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
return "", fmt.Errorf("failed to generate password: %v", err)
}
pwd = append(pwd, chars[randomIndex.Int64()])
}
// Shuffle the password.
mathRand.Shuffle(len(pwd), func(i, j int) { pwd[i], pwd[j] = pwd[j], pwd[i] })
return string(pwd), nil
}