1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-22 14:50:29 +03:00

Advancing on new v2 actor

This commit is contained in:
Adolfo Gómez García 2019-11-26 11:44:58 +01:00
parent 45b827e9e9
commit e967d994b1
11 changed files with 264 additions and 37 deletions

View File

@ -237,6 +237,8 @@ class Actor(Handler):
return 'ok'
raise Exception('Unknown message {} for an user service without os manager'.format(message))
res = osmanager.process(service, message, data, options={'scramble': False})
if not res:
raise Exception('Old Actors not supported by this os Manager!')
except Exception as e:
logger.exception("Exception processing from OS Manager")
return Actor.result(str(e), ERR_OSMANAGER_ERROR)

View File

@ -34,13 +34,28 @@ import secrets
import logging
import typing
from uds.models import getSqlDatetimeAsUnix, getSqlDatetime, ActorToken
from uds.models import (
getSqlDatetimeAsUnix,
getSqlDatetime,
ActorToken,
UserService
)
from uds.core import VERSION
from ..handlers import Handler
from uds.core.util.state import State
from uds.core.util.cache import Cache
from uds.core.util.config import GlobalConfig
from ..handlers import Handler, AccessDenied, RequestError
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from uds.core import osmanagers
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()}
@ -48,6 +63,19 @@ def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -
res['error'] = error
return res
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()
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):
"""
@ -70,7 +98,7 @@ class ActorV2Action(Handler):
path = 'actor/v2'
def get(self):
return actorResult('')
return actorResult(VERSION)
class ActorV2Register(ActorV2Action):
"""
@ -115,15 +143,92 @@ class ActorV2Initiialize(ActorV2Action):
Information about machine action.
Also returns the id used for the rest of the actions. (Only this one will use actor key)
"""
name = 'initiaize'
name = 'initialize'
def post(self):
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]:
"""
Initialize method expect a json POST with this fields:
* version: str -> Actor version
* token: str -> Valid Actor Token (if invalid, will return an error)
* id: List[dict] -> List of dictionary containing id and mac:
Will return on field "result" a dictinary with:
* own_token: Optional[str] -> Personal uuid for the service (That, on service, will be used from now onwards). If None, there is no own_token
* unique_id: Optional[str] -> If not None, unique id for the service
* max_idle: Optional[int] -> If not None, max configured Idle for the vm
* os: Optional[dict] -> Data returned by os manager for setting up this service.
On error, will return Empty (None) result, and error field
Example:
{
'version': '3.0',
'token': 'asbdasdf',
'maxIdle': 99999 or None,
'id': [
{
'mac': 'xxxxx',
'ip': 'vvvvvvvv'
}, ...
]
}
"""
# First, validate token...
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
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:
userService: UserService = next(
iter(UserService.objects.filter(
unique_id__in=[i['mac'] for i in self._params.get('id')[:5]],
state__in=[State.USABLE, State.PREPARING]
))
)
except Exception as e:
logger.info('Unmanaged host request: %s, %s', self._params, e)
return actorResult({
'own_token': None,
'max_idle': None,
'unique_id': None,
'os': None
})
# Managed by UDS, get initialization data from osmanager and return it
# Set last seen actor version
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()
maxIdle = osManager.maxIdle()
logger.debug('Max idle: %s', maxIdle)
osData = osManager.actorData(userService)
return 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 AccessDenied('Access denied')
class ActorV2Login(ActorV2Action):
"""
Information about machine
Notifies user logged out
"""
name = 'login'
@ -133,7 +238,7 @@ class ActorV2Login(ActorV2Action):
class ActorV2Logout(ActorV2Action):
"""
Information about machine
Notifies user logged in
"""
name = 'logout'
@ -143,7 +248,7 @@ class ActorV2Logout(ActorV2Action):
class ActorV2Log(ActorV2Action):
"""
Information about machine
Sends a log from the service
"""
name = 'log'
@ -153,7 +258,7 @@ class ActorV2Log(ActorV2Action):
class ActorV2IpChange(ActorV2Action):
"""
Information about machine
Notifies an IP change
"""
name = 'ipchange'
@ -163,10 +268,20 @@ class ActorV2IpChange(ActorV2Action):
class ActorV2Ready(ActorV2Action):
"""
Information about machine
Notifies the service is ready
"""
name = 'ready'
def post(self):
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')
class ActorV2Ticket(ActorV2Action):
"""
Gets an stored ticket
"""
name = 'ticket'
def post(self):
logger.debug('Args: %s, Params: %s', self._args, self._params)
return actorResult('ok')

View File

@ -95,14 +95,47 @@ class OSManager(Module):
@return nothing
"""
# These methods must be overriden
def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str:
"""
This method must be overriden so your so manager can manage requests and responses from agent.
@param userService: Service that sends the request (virtual machine or whatever)
@param message: message to process (os manager dependent)
@param data: Data for this message
Note: this method is deprecated and will be removed on a future release, when pre 3.0 actors support will be drop
For now, this method will be kept on exising os managers for compatibility with old actors, but is not required for
new os managers (that will only be available on actor 3.0) anymore
"""
return ''
# These methods must be overriden
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
"""
This method provides information to actor, so actor can complete os configuration.
Currently exists 3 types of os managers
* rename vm and do NOT ADD to AD
{
'action': 'rename',
'name': 'xxxxxx'
}
* rename vm and ADD to AD
{
'action': 'renameAD',
'name': 'xxxxxxx',
'ad': 'domain.xxx'
'ou': 'ou' # or '' if default ou
'username': 'userwithaddmachineperms@domain.xxxx'
'password': 'passwordForTheUserWithPerms',
}
* rename vm, do NOT ADD to AD, and change password for an user
{
'action': 'rename_and_pw'
'name': 'xxxxx'
'username': 'username to change pass'
'password': 'current password for username to change password'
'newpassword': 'new password to be set for the username'
}
"""
return {}
def checkState(self, userService: 'UserService') -> str:
"""
@ -240,6 +273,17 @@ class OSManager(Module):
log.useLog('logout', uniqueId, serviceIp, userName, knownUserIP, fullUserName, userService.friendly_name, userService.deployed_service.name)
def loginNotified(self, userService: 'UserService', userName: typing.Optional[str] = None) -> None:
self.loggedIn(userService, userName)
def logoutNotified(self, userService: 'UserService', userName: typing.Optional[str] = None) -> None:
self.loggedOut(userService, userName)
def readyNotified(self, userService: 'UserService') -> None:
"""
Invoked by actor v2 whenever a service is set as "ready"
"""
def isPersistent(self) -> bool:
"""
When a publication if finished, old assigned machines will be removed if this value is True.

View File

@ -48,4 +48,4 @@ class ActorToken(models.Model):
stamp = models.DateTimeField() # Date creation or validation of this entry
def __str__(self):
return '<ActorToken {} created on {} by {} from {}/{}>'.format(self.token, self.stamp, self.username, self.ip_from, self.ip)
return '<ActorToken {} created on {} by {} from {}/{}>'.format(self.token, self.stamp, self.username, self.hostname, self.ip_from)

View File

@ -45,7 +45,7 @@ class UUIDModel(models.Model):
"""
uuid = models.CharField(max_length=50, default=None, null=True, unique=True)
class Meta:
class Meta: # pylint: disable=too-few-public-methods
abstract = True
def genUuid(self) -> str:
@ -53,7 +53,7 @@ class UUIDModel(models.Model):
# Override default save to add uuid
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
if self.uuid is None or self.uuid == '':
if not self.uuid:
self.uuid = self.genUuid()
elif self.uuid != self.uuid.lower():
self.uuid = self.uuid.lower() # If we modify uuid elsewhere, ensure that it's stored in lower case

View File

@ -143,6 +143,28 @@ class LinuxOsManager(osmanagers.OSManager):
except Exception:
log.doLog(service, log.ERROR, "do not understand {0}".format(data), origin)
# default "ready received" does nothing
def readyReceived(self, userService, data):
pass
def loginNotified(self, userService, userName=None):
if '\\' not in userName:
self.loggedIn(userService, userName)
def logoutNotified(self, userService, userName=None):
self.loggedOut(userService, userName)
if self.isRemovableOnLogout(userService):
userService.release()
def readyNotified(self, userService):
return
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename',
'name': userService.getName()
}
def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str:
"""
We understand this messages:

View File

@ -30,6 +30,8 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import random
import string
import logging
import typing
@ -74,8 +76,6 @@ class LinuxRandomPassManager(LinuxOsManager):
return username, password
def genPassword(self, service):
import random
import string
randomPass = service.recoverValue('linOsRandomPass')
if randomPass is None:
randomPass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(16))
@ -90,6 +90,15 @@ class LinuxRandomPassManager(LinuxOsManager):
def infoValue(self, service):
return 'rename\r{0}\t{1}\t\t{2}'.format(self.getName(service), self._userAccount, self.genPassword(service))
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'name': userService.getName(),
'username': self._userAccount,
'password': '', # On linux, user password is not needed so we provide an empty one
'newpassword': self.genPassword(userService)
}
def marshal(self) -> bytes:
"""
Serializes the os manager data so we can store it in database

View File

@ -109,9 +109,6 @@ class WindowsOsManager(osmanagers.OSManager):
pass
def getName(self, userService: 'UserService') -> str:
"""
gets name from deployed
"""
return userService.getName()
def infoVal(self, userService: 'UserService') -> str:
@ -153,6 +150,24 @@ class WindowsOsManager(osmanagers.OSManager):
def readyReceived(self, userService, data):
pass
def loginNotified(self, userService, userName=None):
if '\\' not in userName:
self.loggedIn(userService, userName)
def logoutNotified(self, userService, userName=None):
self.loggedOut(userService, userName)
if self.isRemovableOnLogout(userService):
userService.release()
def readyNotified(self, userService):
return
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename',
'name': userService.getName()
}
def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str: # pylint: disable=too-many-branches
"""
We understand this messages:

View File

@ -156,9 +156,10 @@ class WinDomainOsManager(WindowsOsManager):
raise ldaputil.LDAPError(_str)
def __getGroup(self, ldapConnection: typing.Any) -> str:
def __getGroup(self, ldapConnection: typing.Any) -> typing.Optional[str]:
base = ','.join(['DC=' + i for i in self._domain.split('.')])
group = ldaputil.escape(self._group)
obj: typing.Optional[typing.MutableMapping[str, typing.Any]]
try:
obj = next(ldaputil.getAsDict(ldapConnection, base, "(&(objectClass=group)(|(cn={0})(sAMAccountName={0})))".format(group), ['dn'], sizeLimit=50))
except StopIteration:
@ -176,6 +177,7 @@ class WinDomainOsManager(WindowsOsManager):
base = ','.join(['DC=' + i for i in self._domain.split('.')])
fltr = '(&(objectClass=computer)(sAMAccountName={}$))'.format(ldaputil.escape(machineName))
obj: typing.Optional[typing.MutableMapping[str, typing.Any]]
try:
obj = next(ldaputil.getAsDict(ldapConnection, base, fltr, ['dn'], sizeLimit=50))
except StopIteration:
@ -186,7 +188,7 @@ class WinDomainOsManager(WindowsOsManager):
return obj['dn'] # Returns the DN
def readyReceived(self, userService: 'UserService', data: str) -> None:
def readyNotified(self, userService: 'UserService') -> None:
# No group to add
if self._group == '':
return
@ -227,7 +229,7 @@ class WinDomainOsManager(WindowsOsManager):
log.doLog(userService, log.WARN, error, log.OSMANAGER)
logger.error(error)
def release(self, userService: 'UserSrevice') -> None:
def release(self, userService: 'UserService') -> None:
super().release(userService)
# If no removal requested, just return
@ -266,7 +268,7 @@ class WinDomainOsManager(WindowsOsManager):
try:
ldapConnection = self.__connectLdap()
except ldaputil.LDAPError as e:
return _('Check error: {}').format(self.__getLdapError(e))
return _('Check error: {}').format(e)
except dns.resolver.NXDOMAIN:
return _('Could not find server parameters (_ldap._tcp.{0} can\'t be resolved)').format(self._domain)
except Exception as e:
@ -276,7 +278,7 @@ class WinDomainOsManager(WindowsOsManager):
try:
ldapConnection.search_st(self._ou, ldap.SCOPE_BASE) # @UndefinedVariable
except ldaputil.LDAPError as e:
return _('Check error: {}').format(self.__getLdapError(e))
return _('Check error: {}').format(e)
# Group
if self._group != '':
@ -289,14 +291,13 @@ class WinDomainOsManager(WindowsOsManager):
@staticmethod
def test(env: 'Environment', data: typing.Dict[str, str]) -> typing.List[typing.Any]:
logger.debug('Test invoked')
wd: typing.Optional[WinDomainOsManager] = None
try:
wd = WinDomainOsManager(env, data)
wd: WinDomainOsManager = WinDomainOsManager(env, data)
logger.debug(wd)
try:
ldapConnection = wd.__connectLdap()
except ldaputil.LDAPError as e:
return [False, _('Could not access AD using LDAP ({0})').format(wd.__getLdapError(e))]
return [False, _('Could not access AD using LDAP ({0})').format(e)]
ou = wd._ou
if ou == '':
@ -318,6 +319,16 @@ class WinDomainOsManager(WindowsOsManager):
return [True, _("All parameters seem to work fine.")]
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'name': userService.getName(),
'ad': self._domain,
'ou': self._ou,
'username': self._account,
'password': self._password,
}
def infoVal(self, userService: 'UserService') -> str:
return 'domain:{0}\t{1}\t{2}\t{3}\t{4}'.format(self.getName(userService), self._domain, self._ou, self._account, self._password)
@ -354,7 +365,7 @@ class WinDomainOsManager(WindowsOsManager):
self._serverHint = values[7]
else:
self._serverHint = ''
if values[0] == 'v4':
self._ssl = values[8]
self._removeOnExit = values[9]
@ -373,5 +384,5 @@ class WinDomainOsManager(WindowsOsManager):
dct['grp'] = self._group
dct['serverHint'] = self._serverHint
dct['ssl'] = self._ssl == 'y'
dct['removeOnExit'] = self._removeOnExit == 'y'
dct['removeOnExit'] = self._removeOnExit == 'y'
return dct

View File

@ -96,6 +96,15 @@ class WinRandomPassManager(WindowsOsManager):
log.doLog(userService, log.INFO, "Password set to \"{}\"".format(randomPass), log.OSMANAGER)
return randomPass
def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]:
return {
'action': 'rename_and_pw',
'name': userService.getName(),
'username': self._userAccount,
'password': self._password,
'newpassword': self.genPassword(userService)
}
def infoVal(self, userService: 'UserService') -> str:
return 'rename:{0}\t{1}\t{2}\t{3}'.format(self.getName(userService), self._userAccount, self._password, self.genPassword(userService))

View File

@ -90,10 +90,10 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements
cache = Cache('auth')
cacheKey = str(authenticator.id) + userName
tries = cache.get(cacheKey)
if tries is None:
tries = 0
if authenticator.getInstance().blockUserOnLoginFailures is True and tries >= GlobalConfig.MAX_LOGIN_TRIES.getInt():
tries = cache.get(cacheKey) or 0
triesByIp = cache.get(request.ip) or 0
maxTries = GlobalConfig.MAX_LOGIN_TRIES.getInt()
if (authenticator.getInstance().blockUserOnLoginFailures is True and (tries >= maxTries) or triesByIp >= maxTries):
authLogLogin(request, authenticator, userName, 'Temporarily blocked')
return (None, _('Too many authentication errrors. User temporarily blocked'))
@ -106,8 +106,8 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements
if user is None:
logger.debug("Invalid user %s (access denied)", userName)
tries += 1
cache.put(cacheKey, tries, GlobalConfig.LOGIN_BLOCK.getInt())
cache.put(cacheKey, tries+1, GlobalConfig.LOGIN_BLOCK.getInt())
cache.put(request.ip, triesByIp+1, GlobalConfig.LOGIN_BLOCK.getInt())
authLogLogin(request, authenticator, userName, 'Access denied (user not allowed by UDS)')
return (None, _('Access denied'))