# -*- coding: utf-8 -*-
"""All security in windows is handled via Security Principals. These can
be a user (the most common case), a group of users, a computer, or something
else. Security principals are uniquely identified by their SID: a binary code
represented by a string S-a-b-cd-efg... where each of the segments represents
an aspect of the security authorities involved. (A computer, a domain etc.).
Certain of the SIDs are considered well-known such as the AuthenticatedUsers
account on each machine which will always have the same SID.
Most of the access to this module will be via the :func:`principal`
or :func:`me` functions. Although the module is designed to be used
standalone, it is imported directly into the :mod:`security` module's
namespace so its functionality can also be accessed from there.
"""
from __future__ import unicode_literals
import os, sys
import contextlib
import socket
import ntsecuritycon
import pywintypes
import win32con
import win32security
import win32api
import win32cred
import win32event
import win32net
import win32netcon
import winerror
from winsys._compat import *
from winsys import constants, core, exc, utils
from winsys import _advapi32
__all__ = ['LOGON', 'EXTENDED_NAME', 'x_accounts', 'principal', 'Principal', 'User', 'Group', 'me']
LOGON = constants.Constants.from_pattern("LOGON32_*", namespace=win32security)
LOGON.doc("Types of logon used by LogonUser and related APIs")
EXTENDED_NAME = constants.Constants.from_pattern("Name*", namespace=win32con)
EXTENDED_NAME.doc("Extended display formats for usernames")
WELL_KNOWN_SID = constants.Constants.from_pattern("Win*Sid", namespace=win32security)
WELL_KNOWN_SID.doc("Well-known SIDs common to all computers")
USER_PRIV = constants.Constants.from_list(["USER_PRIV_GUEST", "USER_PRIV_USER", "USER_PRIV_ADMIN"], pattern="USER_PRIV_*", namespace=win32netcon)
USER_PRIV.doc("User-types for creating new users")
UF = constants.Constants.from_pattern("UF_*", namespace=win32netcon)
UF.doc("Flags for creating new users")
SID_NAME_USE = constants.Constants.from_pattern("SidType*", namespace=ntsecuritycon)
SID_NAME_USE.doc("Types of accounts for which SIDs exist")
FILTER = constants.Constants.from_pattern("FILTER_*", namespace=win32netcon)
FILTER.doc("Filters when enumerating users")
PySID = pywintypes.SIDType
[docs]class x_accounts(exc.x_winsys):
"Base for all accounts-related exceptions"
WINERROR_MAP = {
winerror.ERROR_NONE_MAPPED : exc.x_not_found
}
wrapped = exc.wrapper(WINERROR_MAP, x_accounts)
def _win32net_enum(win32_fn, system_or_domain):
resume = 0
while True:
items, total, resume = wrapped(win32_fn, system_or_domain, 0, resume)
for item in items:
yield item
if resume == 0: break
[docs]def principal(principal, cls=core.UNSET):
"""Factory function for the :class:`Principal` class. This is the most
common way to create a :class:`Principal` object::
from winsys import accounts
service_account = accounts.principal (accounts.WELL_KNOWN_SID.Service)
local_admin = accounts.principal ("Administrator")
domain_users = accounts.principal (r"DOMAIN\Domain Users")
:param principal: any of None, a :class:`Principal`, a `PySID`,
a :const:`WELL_KNOWN_SID` or a string
:returns: a :class:`Principal` object corresponding to `principal`
"""
cls = Principal if cls is core.UNSET else cls
if principal is None:
return None
elif type(principal) == PySID:
return cls.from_sid(principal)
elif isinstance(principal, int):
return cls.from_well_known(principal)
elif isinstance(principal, cls):
return principal
else:
return cls.from_string(unicode(principal))
[docs]def user(name):
"""If you know you're after a user, use this. Particularly
useful when a system user is defined as an alias type
"""
return principal(name, cls=User)
[docs]def group(name):
"""If you know you're after a group, use this. Particularly
useful when a system group is defined as an alias type
"""
return principal(name, cls=Group)
def local_group(name):
"""If you know you're after a local group, use this.
"""
return principal(name, cls=LocalGroup)
def global_group(name):
"""If you know you're after a global group, use this.
"""
return principal(name, cls=GlobalGroup)
[docs]def me():
"""Convenience function for the common case of getting the
logged-on user's account.
"""
return Principal.me()
_domain = None
def domain(system=None):
global _domain
if _domain is None:
_domain = wrapped(win32net.NetWkstaGetInfo, system, 100)['langroup']
return _domain
def domain_controller(domain=None):
return wrapped(win32net.NetGetAnyDCName, None, domain)
def users(system=None):
"""Convenience function to yield each of the local users
on a system.
:param system: optional security authority
:returns: yield :class:`User` objects
"""
return iter(_LocalUsers(system))
[docs]class Principal(core._WinSysObject):
"""Object wrapping a Windows security principal, represented by a SID
and, where possible, a name. :class:`Principal` compares and hashes
by SID so can be sorted and used as a dictionary key, set element, etc.
A :class:`Principal` is its own context manager, impersonating the
corresponding user::
from winsys import accounts
with accounts.principal("python"):
print accounts.me()
Note, though, that this will prompt for a password using the
Win32 password UI. To logon with a password, use the :meth:`impersonate`
context-managed function. TODO: allow password to be set securely.
"""
def __init__(self, sid, system=None):
"""Initialise a Principal from and (optionally) a system name. The sid
must be a PySID and the system name, if present must be a security
authority, eg a machine or a domain.
"""
core._WinSysObject.__init__(self)
self.sid = sid
self.system = system
try:
self.name, self.domain, self.type = wrapped(win32security.LookupAccountSid, self.system, self.sid)
except exc.x_not_found:
self.name = str(self.sid)
self.domain = self.type = None
#~ if self.system is None:
#~ self.system = domain_controller(self.domain)
def __hash__(self):
return hash(str(self.sid))
def __eq__(self, other):
return self.sid == principal(other).sid
def __lt__(self, other):
return self.sid < principal(other).sid
[docs] def pyobject(self):
"""Return the internal representation of this object.
:returns: pywin32 SID
"""
return self.sid
def as_string(self):
if self.domain:
return "%s\%s" % (self.domain, self.name)
else:
return self.name or str(self.sid)
def dumped(self, level):
return utils.dumped("user: %s\nsid: %s" % (
self.as_string(),
wrapped(win32security.ConvertSidToStringSid, self.sid)
), level)
[docs] def logon(self, password=core.UNSET, logon_type=core.UNSET):
"""Log on as an authenticated user, returning that
user's token. This is used by security.impersonate
which wraps the token in a Token object and manages
its lifetime in a context.
(EXPERIMENTAL) If no password is given, a UI pops up
to ask for a password.
:param password: the password for this account
:param logon_type: one of the :const:`LOGON` values
:returns: a pywin32 handle to a token
"""
if logon_type is core.UNSET:
logon_type = LOGON.LOGON_NETWORK
else:
logon_type = LOGON.constant(logon_type)
#~ if password is core.UNSET:
#~ password = dialogs.get_password(self.name, self.domain)
hUser = wrapped(
win32security.LogonUser,
self.name,
self.domain,
password,
logon_type,
LOGON.PROVIDER_DEFAULT
)
return hUser
@classmethod
[docs] def from_string(cls, string, system=None):
"""Return a :class:`Principal` based on a name and a
security authority. If `string` is blank, the logged-on user is assumed.
:param string: name of an account in the form "domain\name". domain is optional so the simplest form is simply "name"
:param system: name of a security authority (typically a machine or a domain)
:returns: a :class:`Principal` object for `string`
"""
if string == "":
string = wrapped(win32api.GetUserNameEx, win32con.NameSamCompatible)
sid, domain, type = wrapped(
win32security.LookupAccountName,
None if system is None else unicode(system),
unicode(string)
)
cls = cls.SID_NAME_USE_MAP.get(type, cls)
return cls(sid, None if system is None else unicode(system))
@classmethod
[docs] def from_sid(cls, sid, system=None):
"""Return a :class:`Principal` based on a sid and a security authority.
:param sid: a PySID
:param system_name: optional name of a security authority
:returns: a :class:`Principal` object for `sid`
"""
try:
name, domain, type = wrapped(
win32security.LookupAccountSid,
None if system is None else unicode(system),
sid
)
except exc.x_not_found:
name = domain = type = core.UNSET
cls = cls.SID_NAME_USE_MAP.get(type, cls)
return cls(sid, None if system is None else unicode(system))
@classmethod
[docs] def from_well_known(cls, well_known, domain=None):
"""Return a :class:`Principal` based on one of the :const:`WELL_KNOWN_SID` values.
:param well_known: one of the :const:`WELL_KNOWN_SID`
:param domain: anything accepted by :func:`principal` and corresponding to a domain
"""
return cls.from_sid(wrapped(win32security.CreateWellKnownSid, well_known, principal(domain)))
@classmethod
[docs] def me(cls):
"""Convenience factory method for the common case of referring to the
logged-on user
"""
return cls.from_string(wrapped(win32api.GetUserNameEx, EXTENDED_NAME.SamCompatible))
@contextlib.contextmanager
[docs] def impersonate(self, password=core.UNSET, logon_type=core.UNSET):
"""Context-managed function to impersonate this user and then
revert::
from winsys import accounts, security
print accounts.me()
python = accounts.principal("python")
with python.impersonate("Pa55w0rd"):
print accounts.me()
open("temp.txt", "w").close()
print accounts.me()
security.security("temp.txt").owner == python
Note that the :class:`Principal` class is also its own
context manager but does not allow the password to be specified.
:param password: password for this account
:param logon_type: one of the :const:`LOGON` values
"""
hLogon = self.logon(password, logon_type)
wrapped(win32security.ImpersonateLoggedOnUser, hLogon)
yield hLogon
wrapped(win32security.RevertToSelf)
def __enter__(self):
wrapped(win32security.ImpersonateLoggedOnUser, self.logon(logon_type=LOGON.LOGON_INTERACTIVE))
def __exit__(self, *exc_info):
wrapped(win32security.RevertToSelf)
[docs]class User(Principal):
@classmethod
[docs] def create(cls, username, password, system=None):
"""Create a new user with `username` and `password`. Return
a :class:`User` for the new user.
:param username: username of the new user. Must not already exist on `system`
:param password: password for the new user. Must meet security policy on `system`
:param system: optional system name
:returns: a :class:`User` for `username`
"""
user_info = dict(
name = username,
password = password,
priv = USER_PRIV.USER,
home_dir = None,
comment = None,
flags = UF.SCRIPT,
script_path = None
)
wrapped(win32net.NetUserAdd, system, 1, user_info)
return cls.from_string(username, system)
[docs] def delete(self):
"""Delete this user from `system`.
:param system: optional security authority
"""
wrapped(win32net.NetUserDel, self.system, self.name)
[docs] def groups(self):
"""Yield the groups this user belongs to
:param system: optional security authority
"""
for group_name, attributes in wrapped(win32net.NetUserGetGroups, self.system, self.name):
yield group(group_name)
for group_name in wrapped(win32net.NetUserGetLocalGroups, self.system, self.name):
yield group(group_name)
[docs] def join(self, other_group):
"""Add this user to a group
:param other_group: anything accepted by :func:`group`
:returns: self
"""
return group(other_group).add(self)
[docs] def leave(self, other_group):
"""Remove this user from a group
:param other_group: anything accepted by :func:`group`
:returns: self
"""
return group(other_group).remove(self)
[docs] def runas(self, command_line, password=core.UNSET, load_profile=False):
"""Run a command logged on as this user
:param command_line: command line to run, quoted as necessary
:param password: password; if not supplied, standard Windows prompt
:param with_profile: if True, HKEY_CURRENT_USER is loaded [False]
"""
#~ if not password:
#~ password = dialogs.get_password(self.name, self.domain)
logon_flags = 0
if load_profile: logon_flags |= _advapi32.LOGON_FLAGS.WITH_PROFILE
process_info = _advapi32.CreateProcessWithLogonW(
username=self.name,
domain=self.domain,
password=password,
command_line=command_line,
logon_flags=logon_flags
)
#
# Wait for up to 20 secs
#
#~ if wrapped(win32event.WaitForInputIdle, process_info.hProcess, 10000) == win32event.WAIT_TIMEOUT:
#~ raise x_accounts(errctx="runas", errmsg="runas process not created with 10 secs")
[docs]class Group(Principal):
SID_NAME_USE_MAP = {}
def __contains__(self, member):
"""Crudely, iterate over the group's members until you hit `member`
"""
member = principal(member)
return any(member == m for m in self)
class GlobalGroup(Group):
_enumerator = win32net.NetGroupEnum
@classmethod
def create(cls, groupname, domain=None):
"""Create a new group. Return a :class:`Group` for the new group.
:param groupname: name of the new group. Must not already exist on `system`
:param system: optional security authority
:returns: a :class:`Group` for `groupname`
"""
system = domain_controller(domain)
wrapped(win32net.NetGroupAdd, system, 0, dict(name=groupname))
return cls.from_string(groupname, system)
def delete(self):
"""Delete this group from `system`.
:param system: optional security authority
"""
wrapped(win32net.NetGroupDel, self.system, self.name)
def add(self, member):
"""Add a :class:`Principal` to this local group
:param member: anything accepted by :func:`principal`
:returns: :class:`Principal` for `member`
"""
member = principal(member)
wrapped(win32net.NetGroupAddUser, self.system, self.name, r"%s\%s" % (member.domain, member.name))
return member
def remove(self, member):
"""Remove a :class:`Principal` from this local group. The
principal must already be a member of the group.
:param member: anything accepted by :func:`principal`
:returns: :class:`Principal` for `member`
"""
member = principal(member)
wrapped(win32net.NetGroupDelUser, self.system, self.name, r"%s\%s" % (member.domain, member.name))
return member
def __iter__(self):
"""Yield the list of members of this group.
:returns: yield a :class:`Principal` or subclass corresponding to each member
of this group
"""
resume = 0
while True:
members, total, resume = wrapped(win32net.NetGroupGetUsers, self.system, self.name, 1, resume)
for member in members:
yield principal(member['name'])
if resume == 0: break
class LocalGroup(Group):
@classmethod
def create(cls, groupname, system=None):
"""Create a new group. Return a :class:`Group` for the new group.
:param groupname: name of the new group. Must not already exist on `system`
:param system: optional security authority
:returns: a :class:`Group` for `groupname`
"""
wrapped(win32net.NetLocalGroupAdd, system, 0, dict(name=groupname))
return cls.from_string(groupname, system)
def delete(self):
"""Delete this group from `system`.
:param system: optional security authority
"""
wrapped(win32net.NetLocalGroupDel, self.system, self.name)
def add(self, member):
"""Add a :class:`Principal` to this local group
:param member: anything accepted by :func:`principal`
:returns: :class:`Principal` for `member`
"""
member = principal(member)
wrapped(win32net.NetLocalGroupAddMembers, self.system, self.name, 0, [dict(sid=member.sid)])
return member
def remove(self, member):
"""Remove a :class:`Principal` from this local group. The
principal must already be a member of the group.
:param member: anything accepted by :func:`principal`
:returns: :class:`Principal` for `member`
"""
member = principal(member)
wrapped(win32net.NetLocalGroupDelMembers, self.system, self.name, ["%s\\%s" % (member.domain, member.name)])
return member
def __iter__(self):
"""Yield the list of members of this group.
:returns: yield a :class:`Principal` or subclass corresponding to each member
of this group
"""
resume = 0
while True:
members, total, resume = wrapped(win32net.NetLocalGroupGetMembers, self.system, self.name, resume)
for member in members:
yield principal(member['sid'])
if resume == 0: break
Principal.SID_NAME_USE_MAP = {
SID_NAME_USE.User : User,
SID_NAME_USE.Group : Group,
SID_NAME_USE.WellKnownGroup : Group
}
def local_groups(system=None):
"""Convenience function to yield each of the local users
on a system.
:param system: optional security authority
:returns: yield :class:`LocalGroup` objects
"""
for group in _win32net_enum(win32net.NetLocalGroupEnum, system):
yield LocalGroup.from_string(group['name'])
def global_groups(domain=None):
"""Convenience function to yield each of the local users
on a system.
:param domain: optional security domain
:returns: yield :class:`GlobalGroup` objects
"""
for group in _win32net_enum(win32net.NetGroupEnum, domain_controller(domain)):
yield GlobalGroup.from_string(group['name'])
class _LocalUsers(object):
def __init__(self, system=None):
self.system = system
def __iter__(self):
resume = 0
while True:
users, total, resume = wrapped(win32net.NetUserEnum, self.system, 0, FILTER.NORMAL_ACCOUNT, resume)
for user in users:
yield User.from_string(user['name'])
if resume == 0: break
def add(self, username, password):
return User.create(username, password)
def remove(self, local_user):
return user(local_user).delete()