advancing on new v3 actor

This commit is contained in:
Adolfo Gómez García 2019-11-30 22:34:05 +01:00
parent a8b94325e5
commit ced7226f81
11 changed files with 214 additions and 170 deletions

View File

@ -72,7 +72,7 @@ class PublicProvider(handler.Handler):
logger.debug('Received Pre connection')
if 'user' not in self._params or 'protocol' not in self._params:
raise Exception('Invalid preConnect parameters')
return self._service.preConnect(self._params['user'], self._params['protocol'])
return self._service.preConnect(self._params['user'], self._params['protocol'], self._params.get('ip', 'unknown'), self._params.get('hostname', 'unknown'))
def get_information(self) -> typing.Any:
# Return something useful? :)

View File

@ -51,10 +51,10 @@ class UDSActorSvc(daemon.Daemon, CommonService):
daemon.Daemon.__init__(self, '/var/run/udsactor.pid')
CommonService.__init__(self)
# Captures signals so we can stop gracefully
signal.signal(signal.SIGINT, self.markForExit)
signal.signal(signal.SIGTERM, self.markForExit)
def markForExit(self, signum, frame) -> None:
self._isAlive = False

View File

@ -90,12 +90,12 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
# Ensures exists destination folder
dirname = os.path.dirname(CONFIGFILE)
if not os.path.exists(dirname):
os.mkdir(dirname, mode=0o700) # Will create only if route to path already exists, for example, /etc (that must... :-))
os.mkdir(dirname, mode=0o755) # Will create only if route to path already exists, for example, /etc (that must... :-))
with open(CONFIGFILE, 'w') as f:
cfg.write(f)
os.chmod(CONFIGFILE, 0o0600) # Ensure only readable by root
os.chmod(CONFIGFILE, 0o0644) # Ensure only readable by root
def useOldJoinSystem() -> bool:

View File

@ -58,34 +58,6 @@ class RESTUserServiceNotFoundError(RESTError):
class RESTOsManagerError(RESTError):
ERRCODE = 4
# Disable warnings log messages
try:
import urllib3 # @UnusedImport @UnresolvedImport
except Exception:
from requests.packages import urllib3 # @Reimport @UnresolvedImport
try:
urllib3.disable_warnings() # @UndefinedVariable
warnings.simplefilter("ignore")
except Exception:
pass # In fact, isn't too important, but will log warns to logging file
# Constants
def ensureResultIsOk(result: typing.Any) -> None:
if 'error' not in result:
return
for i in (RESTInvalidKeyError, RESTUnmanagedHostError, RESTUserServiceNotFoundError, RESTOsManagerError):
if result['error'] == i.ERRCODE:
raise i(result['result'])
err = RESTError(result['result'])
err.ERRCODE = result['error']
raise err
class REST:
def __init__(self, host: str, validateCert: bool) -> None:
self.host = host
@ -93,7 +65,7 @@ class REST:
self.url = "https://{}/uds/rest/".format(self.host)
# Disable logging requests messages except for errors, ...
logging.getLogger("requests").setLevel(logging.CRITICAL)
# Tries to disable all warnings
logging.getLogger("urllib3").setLevel(logging.ERROR)
try:
warnings.simplefilter("ignore") # Disables all warnings
except Exception:
@ -103,6 +75,24 @@ class REST:
def _headers(self) -> typing.MutableMapping[str, str]:
return {'content-type': 'application/json'}
def _actorPost(
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any],
headers: typing.Optional[typing.MutableMapping[str, str]] = None
) -> typing.Any:
headers = headers or self._headers
try:
result = requests.post(self.url + 'actor/v2/' + method, data=json.dumps(payLoad), headers=headers, verify=self.validateCert)
if result.ok:
return result.json()['result']
except requests.ConnectionError as e:
raise RESTConnectionError(str(e))
except Exception as e:
pass
raise RESTError(result.content)
def _login(self, auth: str, username: str, password: str) -> typing.MutableMapping[str, str]:
try:
# First, try to login
@ -120,7 +110,6 @@ class REST:
return headers
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try:
result = requests.get(self.url + 'auth/auths', headers=self._headers, verify=self.validateCert, timeout=4)
@ -137,7 +126,6 @@ class REST:
except Exception:
pass
def register( #pylint: disable=too-many-arguments, too-many-locals
self,
auth: str,
@ -186,36 +174,31 @@ class REST:
'version': VERSION,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
}
try:
result = requests.post(self.url + 'actor/v2/initialize', data=json.dumps(payload), headers=self._headers, verify=self.validateCert)
if result.ok:
r = result.json()['result']
os = r['os']
return types.InitializationResultType(
own_token=r['own_token'],
unique_id=r['unique_id'].lower() if r['unique_id'] else None,
max_idle=r['max_idle'],
os=types.ActorOsConfigurationType(
action=os['action'],
name=os['name'],
username=os.get('username'),
password=os.get('password'),
new_password=os.get('new_password'),
ad=os.get('ad'),
ou=os.get('ou')
) if r['os'] else None
)
except requests.ConnectionError as e:
raise RESTConnectionError(str(e))
except Exception as e:
pass
r = self._actorPost('initialize', payload)
os = r['os']
return types.InitializationResultType(
own_token=r['own_token'],
unique_id=r['unique_id'].lower() if r['unique_id'] else None,
max_idle=r['max_idle'],
os=types.ActorOsConfigurationType(
action=os['action'],
name=os['name'],
username=os.get('username'),
password=os.get('password'),
new_password=os.get('new_password'),
ad=os.get('ad'),
ou=os.get('ou')
) if r['os'] else None
)
raise RESTError(result.content)
def ready(self, own_token: str, secret: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None:
# TODO: implement ready
return
def ready(self, own_token: str, secret: str, ip: str) -> None:
payload = {
'token': own_token,
'secret': secret,
'ip': ip
}
self._actorPost('ready', payload) # Ignores result...
def notifyIpChange(self, own_token: str, secret: str, ip: str) -> None:
# TODO: implement notifyIpChange
return
# In fact, notifyingIpChange is same as ready right now
self.ready(own_token, secret, ip)

View File

@ -60,6 +60,8 @@ class CommonService:
_isAlive: bool = True
_rebootRequested: bool = False
_loggedIn = False
_cachedInteface: typing.Optional[types.InterfaceInfoType] = None
_cfg: types.ActorConfigurationType
_api: rest.REST
_interfaces: typing.List[types.InterfaceInfoType]
@ -83,6 +85,19 @@ class CommonService:
socket.setdefaulttimeout(20)
def serviceInterfaceInfo(self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None) -> typing.Optional[types.InterfaceInfoType]:
"""
returns the inteface with unique_id mac or first interface or None if no interfaces...
"""
interfaces = interfaces or self._interfaces
if self._cfg.config and interfaces:
try:
return next(x for x in self._interfaces if x.mac.lower() == self._cfg.config.unique_id)
except StopIteration:
return interfaces[0]
return None
def reboot(self) -> None:
self._rebootRequested = True
@ -94,8 +109,13 @@ class CommonService:
platform.store.writeConfig(self._cfg)
if self._cfg.own_token and self._interfaces:
self._api.ready(self._cfg.own_token, self._secret, self._interfaces)
# Cleans sensible data
srvInterface = self.serviceInterfaceInfo()
if srvInterface:
self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip)
else:
logger.error('Could not locate IP address!!!. (Not registered with UDS)')
# Cleans sensible data
if self._cfg.config:
self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
platform.store.writeConfig(self._cfg)
@ -183,17 +203,10 @@ class CommonService:
# Not enouth data do check
return
unique_id = self._cfg.config.unique_id
def locateMac(interfaces: typing.Iterable[types.InterfaceInfoType]) -> typing.Optional[types.InterfaceInfoType]:
try:
return next(x for x in interfaces if x.mac.lower() == unique_id)
except StopIteration:
return None
try:
old: types.InterfaceInfoType = locateMac(self._interfaces)
new: types.InterfaceInfoType = locateMac(platform.operations.getNetworkInfo())
if not new:
old = self.serviceInterfaceInfo()
new = self.serviceInterfaceInfo(platform.operations.getNetworkInfo())
if not new or not old:
raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
if old.ip != new.ip:
self._api.notifyIpChange(self._cfg.own_token, self._secret, new.ip)
@ -265,7 +278,7 @@ class CommonService:
'''
logger.info('Service is being stopped')
def preConnect(self, userName: str, protocol: str) -> str: # pylint: disable=unused-argument
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str: # pylint: disable=unused-argument
'''
Invoked when received a PRE Connection request via REST
Base preconnect executes the preconnect command

View File

@ -147,7 +147,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
logger.info('Using multiple step join because configuration requests to do so')
self.multiStepJoin(name, domain, ou, account, password)
def preConnect(self, userName: str, protocol: str) -> str:
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str:
logger.debug('Pre connect invoked')
if protocol == 'rdp': # If connection is not using rdp, skip adding user
@ -176,7 +176,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
self._user = None
logger.debug('User {} already in group'.format(userName))
return super().preConnect(userName, protocol)
return super().preConnect(userName, protocol, ip, hostname)
def ovLogon(self, username: str, password: str) -> str:
"""

View File

@ -76,7 +76,7 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
except Exception:
key = wreg.CreateKeyEx(BASEKEY, PATH, 0, wreg.KEY_ALL_ACCESS) # @UndefinedVariable
fixRegistryPermissions(key.handle)
# fixRegistryPermissions(key.handle)
wreg.SetValueEx(key, "", 0, wreg.REG_BINARY, pickle.dumps(config)) # @UndefinedVariable
wreg.CloseKey(key) # @UndefinedVariable

View File

@ -41,7 +41,8 @@ from uds.models import (
UserService
)
from uds.core import VERSION
#from uds.core import VERSION
from uds.core.managers import userServiceManager
from uds.core.util.state import State
from uds.core.util.cache import Cache
from uds.core.util.config import GlobalConfig
@ -56,49 +57,65 @@ logger = logging.getLogger(__name__)
ALLOWED_FAILS = 5
def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -> typing.MutableMapping[str, typing.Any]:
result = result or ''
res = {'result': result, 'stamp': getSqlDatetimeAsUnix()}
if error:
res['error'] = error
return res
class BlockAccess(Exception):
pass
# Helpers
def checkBlockedIp(ip: str)-> None:
cache = Cache('actorv2')
fails = cache.get(ip) or 0
if fails > ALLOWED_FAILS:
logger.info('Access to actor from %s is blocked for %s seconds since last fail', ip, GlobalConfig.LOGIN_BLOCK.getInt())
raise Exception()
raise BlockAccess()
def incFailedIp(ip: str) -> None:
cache = Cache('actorv2')
fails = (cache.get(ip) or 0) + 1
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
# Enclosed methods under /actor path
class ActorV2(Handler):
"""
Processes actor requests
"""
authenticated = False # Actor requests are not authenticated by REST api (except register)
path = 'actor'
name = 'v2'
def get(self):
"""
Processes get requests
"""
logger.debug('Actor args for GET: %s', self._args)
return actorResult({'version': VERSION, 'required': '3.0.0'})
class ActorV2Action(Handler):
authenticated = False # Actor requests are not authenticated normally
path = 'actor/v2'
def get(self):
return actorResult(VERSION)
@staticmethod
def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -> typing.MutableMapping[str, typing.Any]:
result = result or ''
res = {'result': result, 'stamp': getSqlDatetimeAsUnix()}
if error:
res['error'] = error
return res
@staticmethod
def setCommsUrl(userService: UserService, ip: str, secret: str):
url = 'https://{}/actor/{}'.format(userService.getLoggedIP(), secret)
userService.setCommsUrl(url)
def getUserService(self) -> UserService:
'''
Looks for an userService and, if not found, raises a BlockAccess request
'''
try:
return UserService.objects.get(uuid=self._params['token'])
except UserService.DoesNotExist:
raise BlockAccess()
def action(self) -> typing.MutableMapping[str, typing.Any]:
return ActorV2Action.actorResult(error='Base action invoked')
def post(self) -> typing.MutableMapping[str, typing.Any]:
try:
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
result = self.action()
logger.debug('Action result: %s', result)
return result
except BlockAccess:
# For blocking attacks
incFailedIp(self._request.ip) # pylint: disable=protected-access
except Exception:
logger.exception('Posting')
pass
raise AccessDenied('Access denied')
class ActorV2Register(ActorV2Action):
"""
@ -136,7 +153,7 @@ class ActorV2Register(ActorV2Action):
token=secrets.token_urlsafe(36),
stamp=getSqlDatetime()
)
return actorResult(actorToken.token)
return ActorV2Action.actorResult(actorToken.token)
class ActorV2Initiialize(ActorV2Action):
"""
@ -145,17 +162,7 @@ class ActorV2Initiialize(ActorV2Action):
"""
name = 'initialize'
def get(self) -> typing.MutableMapping[str, typing.Any]:
"""
Processes get requests. Basically checks if this is a "postThoughGet" for OpenGnsys or similar
"""
if self._args[0] == 'PostThoughGet':
self._args = self._args[1:] # Remove first argument
return self.post()
raise RequestError('Invalid request')
def post(self) -> typing.MutableMapping[str, typing.Any]:
def action(self) -> typing.MutableMapping[str, typing.Any]:
"""
Initialize method expect a json POST with this fields:
* version: str -> Actor version
@ -183,7 +190,6 @@ class ActorV2Initiialize(ActorV2Action):
# First, validate token...
logger.debug('Args: %s, Params: %s', self._args, self._params)
try:
checkBlockedIp(self._request.ip) # Raises an exception if ip is temporarily blocked
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
# Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided.
try:
@ -195,7 +201,7 @@ class ActorV2Initiialize(ActorV2Action):
)
except Exception as e:
logger.info('Unmanaged host request: %s, %s', self._params, e)
return actorResult({
return ActorV2Action.actorResult({
'own_token': None,
'max_idle': None,
'unique_id': None,
@ -207,44 +213,88 @@ class ActorV2Initiialize(ActorV2Action):
userService.setProperty('actor_version', self._params['version'])
maxIdle = None
osData: typing.MutableMapping[str, typing.Any] = {}
if userService.deployed_service.osmanager:
osManager: 'osmanagers.OSManager' = userService.deployed_service.osmanager.getInstance()
osManager: typing.Optional['osmanagers.OSManager'] = userService.getOsManager()
if osManager:
maxIdle = osManager.maxIdle()
logger.debug('Max idle: %s', maxIdle)
osData = osManager.actorData(userService)
return actorResult({
return ActorV2Action.actorResult({
'own_token': userService.uuid,
'unique_id': userService.unique_id,
'max_idle': maxIdle,
'os': osData
})
except ActorToken.DoesNotExist:
incFailedIp(self._request.ip) # For blocking attacks
except Exception:
pass
raise BlockAccess()
raise AccessDenied('Access denied')
class ActorV2Ready(ActorV2Action):
"""
Notifies the user service is ready
"""
name = 'ready'
def action(self) -> typing.MutableMapping[str, typing.Any]:
"""
Initialize method expect a json POST with this fields:
* token: str -> Valid Actor "own_token" (if invalid, will return an error).
Currently it is the same as user service uuid, but this could change
* secret: Secret for commsUrl for actor
* ip: ip accesible by uds
"""
logger.debug('Args: %s, Params: %s', self._args, self._params)
userService = self.getUserService()
# Stores known IP and notifies it to deployment
userService.logIP(self._params['ip'])
userServiceInstance = userService.getInstance()
userServiceInstance.setIp(self._params['ip'])
userService.updateData(userServiceInstance)
# Store communications url also
ActorV2Action.setCommsUrl(userService, self._params['ip'], self._params['secret'])
if userService.os_state != State.USABLE:
userService.setOsState(State.USABLE)
# Notify osManager or readyness if has os manager
osManager: typing.Optional['osmanagers.OSManager'] = userService.getOsManager()
if osManager:
osManager.toReady(userService)
userServiceManager().notifyReadyFromOsManager(userService, '')
return ActorV2Action.actorResult('ok')
class ActorV2Login(ActorV2Action):
"""
Notifies user logged out
Notifies user logged id
"""
name = 'login'
def post(self):
def action(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
userService = self.getUserService()
osManager: typing.Optional['osmanagers.OSManager'] = userService.getOsManager()
if osManager:
osManager.loggedIn(userService, self._params.get('username') or '')
ip, hostname = userService.getConnectionSource()
deadLine = userService.deployed_service.getDeadline()
return ActorV2Action.actorResult({
'ip': ip,
'hostname': hostname,
'dead_line': deadLine
})
class ActorV2Logout(ActorV2Action):
"""
Notifies user logged in
Notifies user logged out
"""
name = 'logout'
def post(self):
def action(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
return ActorV2Action.actorResult('ok')
class ActorV2Log(ActorV2Action):
"""
@ -252,36 +302,9 @@ class ActorV2Log(ActorV2Action):
"""
name = 'log'
def post(self):
def action(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
class ActorV2Ready(ActorV2Action):
"""
Notifies the service is ready
"""
name = 'ready'
def setCommsUrl(self, userService: UserService, secret: str):
url = 'https://{}/actor/{}'.format(userService.getLoggedIP(), secret)
userService.setCommsUrl(url)
def post(self):
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
class ActorV2IpChange(ActorV2Action):
"""
Notifies an IP change
"""
name = 'ipchange'
def post(self):
"""
Records the ip change, and also fix notifyComms url
"""
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
return ActorV2Action.actorResult('ok')
class ActorV2Ticket(ActorV2Action):
"""
@ -289,6 +312,29 @@ class ActorV2Ticket(ActorV2Action):
"""
name = 'ticket'
def post(self):
def action(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
return ActorV2Action.actorResult('ok')
class ActorV2Notify(ActorV2Action):
name = 'notify'
def post(self) -> typing.MutableMapping[str, typing.Any]:
raise AccessDenied('Access denied')
def get(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
if 'action' not in self._params or 'token' not in self._params or self._params['action'] not in ('login', 'logout'):
# Requested login or logout
raise RequestError('Invalid parameters')
try:
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
userService = UserService.objects.get(uuid=self._params['token'])
# TODO: finish this
return ActorV2Action.actorResult('ok')
except UserService.DoesNotExist:
# For blocking attacks
incFailedIp(self._request.ip) # pylint: disable=protected-access
raise AccessDenied('Access denied')

View File

@ -455,6 +455,8 @@ class UserServiceManager:
'''
proxy = userService.deployed_service.proxy
url = userService.getCommsUrl()
ip, hostname = userService.getConnectionSource()
if not url:
logger.debug('No notification is made because agent does not supports notifications')
return
@ -462,7 +464,7 @@ class UserServiceManager:
url += '/preConnect'
try:
data = {'user': userName, 'protocol': protocol}
data = {'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname}
if proxy is not None:
r = proxy.doProxyRequest(url=url, data=data, timeout=2)
else:

View File

@ -259,7 +259,7 @@ class UserService(UUIDModel): # pylint: disable=too-many-public-methods
return (self.src_ip, self.src_hostname)
def getOsManager(self) -> typing.Optional['OSManager']:
return self.deployed_service.osmanager
return self.deployed_service.osmanager.getInstance()
def needsOsManager(self) -> bool:
"""

View File

@ -71,7 +71,7 @@ class WinDomainOsManager(WindowsOsManager):
grp = gui.TextField(length=64, label=_('Machine Group'), order=7, tooltip=_('Group to which add machines on creation. If empty, no group will be used. (experimental)'), tab=_('Advanced'))
removeOnExit = gui.CheckBoxField(label=_('Machine clean'), order=8, tooltip=_('If checked, UDS will try to remove the machine from the domain USING the provided credentials'), tab=_('Advanced'), defvalue=gui.TRUE)
serverHint = gui.TextField(length=64, label=_('Server Hint'), order=9, tooltip=_('In case of several AD servers, which one is preferred'), tab=_('Advanced'))
ssl = gui.CheckBoxField(label=_('Use SSL'), order=10, tooltip=_('If checked, a ssl connection to Active Directory will be used'), tab=_('Advanced'))
ssl = gui.CheckBoxField(label=_('Use SSL'), order=10, tooltip=_('If checked, a ssl connection to Active Directory will be used'), tab=_('Advanced'), defvalue=gui.TRUE)
# Inherits base "onLogout"
onLogout = WindowsOsManager.onLogout