working on allowing services to detect user login on unmanaged services

This commit is contained in:
Adolfo Gómez García 2020-11-08 19:17:29 +01:00
parent 00dc4c5a7b
commit 74ad50d7d8
6 changed files with 226 additions and 65 deletions

View File

@ -186,9 +186,9 @@ def getCurrentUser() -> str:
def getSessionType() -> str:
'''
Known values:
* Unknown -> No SESSIONNAME environment variable
* Console -> Local session
* RDP-Tcp#[0-9]+ -> RDP Session
* Unknown -> No XDG_SESSION_TYPE environment variable
* xrdp --> xrdp session
* other types
'''
return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown')

View File

@ -48,28 +48,37 @@ TIMEOUT = 5 # 5 seconds is more than enought
# Constants
UNKNOWN = 'unknown'
class RESTError(Exception):
ERRCODE = 0
class RESTConnectionError(RESTError):
ERRCODE = -1
# Errors ""raised"" from broker
class RESTInvalidKeyError(RESTError):
ERRCODE = 1
class RESTUnmanagedHostError(RESTError):
ERRCODE = 2
class RESTUserServiceNotFoundError(RESTError):
ERRCODE = 3
class RESTOsManagerError(RESTError):
ERRCODE = 4
#
# Basic UDS Api
#
class UDSApi: # pylint: disable=too-few-public-methods
"""
Base for remote api accesses
@ -101,11 +110,11 @@ class UDSApi: # pylint: disable=too-few-public-methods
raise NotImplementedError
def _doPost(
self,
method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any],
headers: typing.Optional[typing.MutableMapping[str, str]] = None
) -> typing.Any:
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._apiURL(method), data=json.dumps(payLoad), headers=headers, verify=self._validateCert, timeout=TIMEOUT)
@ -128,6 +137,8 @@ class UDSApi: # pylint: disable=too-few-public-methods
#
# UDS Broker API access
#
class UDSServerApi(UDSApi):
def _apiURL(self, method: str) -> str:
return self._url + 'actor/v3/' + method
@ -148,19 +159,19 @@ class UDSServerApi(UDSApi):
except Exception:
pass
def register( #pylint: disable=too-many-arguments, too-many-locals
self,
auth: str,
username: str,
password: str,
hostname: str,
ip: str,
mac: str,
preCommand: str,
runOnceCommand: str,
postCommand: str,
logLevel: int
) -> str:
def register( # pylint: disable=too-many-arguments, too-many-locals
self,
auth: str,
username: str,
password: str,
hostname: str,
ip: str,
mac: str,
preCommand: str,
runOnceCommand: str,
postCommand: str,
logLevel: int
) -> str:
"""
Raises an exception if could not register, or registers and returns the "authorization token"
"""
@ -198,10 +209,10 @@ class UDSServerApi(UDSApi):
raise RESTError(result.content.decode())
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actorType: typing.Optional[str]) -> types.InitializationResultType:
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actor_type: typing.Optional[str]) -> types.InitializationResultType:
# Generate id list from netork cards
payload = {
'type': actorType or types.MANAGED,
'type': actor_type or types.MANAGED,
'token': token,
'version': VERSION,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
@ -267,9 +278,16 @@ class UDSServerApi(UDSApi):
password=result['password']
)
def login(self, own_token: str, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
if not own_token:
def login(
self,
actor_type: typing.Optional[str],
token: str,
username: str,
sessionType: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str]
) -> types.LoginResultInfoType:
if not token:
return types.LoginResultInfoType(
ip='0.0.0.0',
hostname=UNKNOWN,
@ -277,9 +295,12 @@ class UDSServerApi(UDSApi):
max_idle=None
)
payload = {
'token': own_token,
'type': actor_type or types.MANAGED,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': token,
'username': username,
'session_type': sessionType or UNKNOWN
'session_type': sessionType,
'secret': secret or '',
}
result = self._doPost('login', payload)
return types.LoginResultInfoType(
@ -289,16 +310,25 @@ class UDSServerApi(UDSApi):
max_idle=result['max_idle']
)
def logout(self, own_token: str, username: str) -> None:
if not own_token:
def logout(
self,
actor_type: typing.Optional[str],
token: str,
username: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str]
) -> None:
if not token:
return
payload = {
'token': own_token,
'username': username
'type': actor_type or types.MANAGED,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': token,
'username': username,
'secret': secret or ''
}
self._doPost('logout', payload)
def log(self, own_token: str, level: int, message: str) -> None:
if not own_token:
return

View File

@ -273,7 +273,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
return False
# Only removes token for managed machines
# Only removes master token for managed machines (will need it on next client execution)
master_token = None if self.isManaged() else self._cfg.master_token
self._cfg = self._cfg._replace(
master_token=master_token,
@ -314,7 +314,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if self._loggedIn and self._cfg.own_token:
self._loggedIn = False
try:
self._api.logout(self._cfg.own_token, '')
self._api.logout(
self._cfg.actorType,
self._cfg.own_token,
'',
self._interfaces
)
except Exception as e:
logger.error('Error notifying final logout to UDS: %s', e)
@ -405,18 +410,46 @@ class CommonService: # pylint: disable=too-many-instance-attributes
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
self._loggedIn = True
master_token = None
secret = None
# If unmanaged, do initialization now, because we don't know before this
# Also, even if not initialized, get a "login" notification token
if not self.isManaged():
self.initialize()
master_token = self._cfg.master_token
secret = self._secret
if self._cfg.own_token:
result = self._api.login(self._cfg.own_token, username, sessionType)
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
result = self._api.login(
self._cfg.actorType,
token,
username,
sessionType or '',
self._interfaces,
secret
)
return result
def logout(self, username: str) -> None:
self._loggedIn = False
if self._cfg.own_token:
self._api.logout(self._cfg.own_token, username)
master_token = self._cfg.master_token if self.isManaged() else None
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
self._api.logout(
self._cfg.actorType,
token,
username,
self._interfaces
)
self.onLogout(username)

View File

@ -32,6 +32,7 @@
import secrets
import logging
import typing
from uds.models import user
from uds.models import (
getSqlDatetimeAsUnix,
@ -61,22 +62,27 @@ logger = logging.getLogger(__name__)
ALLOWED_FAILS = 5
UNMANAGED = 'unmanaged' # matches the definition of UDS Actors OFC
class BlockAccess(Exception):
pass
# Helpers
def checkBlockedIp(ip: str)-> None:
def checkBlockedIp(ip: str) -> None:
cache = Cache('actorv3')
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 BlockAccess()
def incFailedIp(ip: str) -> None:
cache = Cache('actorv3')
fails = (cache.get(ip) or 0) + 1
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
class ActorV3Action(Handler):
authenticated = False # Actor requests are not authenticated normally
path = 'actor/v3'
@ -119,6 +125,7 @@ class ActorV3Action(Handler):
raise AccessDenied('Access denied')
class Test(ActorV3Action):
"""
Tests UDS Broker actor connectivity & key
@ -137,6 +144,7 @@ class Test(ActorV3Action):
return ActorV3Action.actorResult('ok')
class Register(ActorV3Action):
"""
Registers an actor
@ -177,6 +185,7 @@ class Register(ActorV3Action):
)
return ActorV3Action.actorResult(actorToken.token)
class Initiialize(ActorV3Action):
"""
Information about machine action.
@ -267,6 +276,7 @@ class Initiialize(ActorV3Action):
except (ActorToken.DoesNotExist, Service.DoesNotExist):
raise BlockAccess()
class BaseReadyChange(ActorV3Action):
"""
Records the IP change of actor
@ -318,12 +328,14 @@ class BaseReadyChange(ActorV3Action):
return ActorV3Action.actorResult({'private_key': privateKey, 'server_certificate': cert, 'password': password})
class IpChange(BaseReadyChange):
"""
Processses IP Change.
"""
name = 'ipchange'
class Ready(BaseReadyChange):
"""
Notifies the user service is ready
@ -352,6 +364,7 @@ class Ready(BaseReadyChange):
return result
class Version(ActorV3Action):
"""
Notifies the version.
@ -367,26 +380,75 @@ class Version(ActorV3Action):
return ActorV3Action.actorResult()
class LoginLogout(ActorV3Action):
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
class Login(ActorV3Action):
def notifyService(self, login: bool):
try:
# If unmanaged, use Service locator
service : 'services.Service' = Service.objects.get(token=self._params['token']).getInstance()
# Locate an userService that belongs to this service and which
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
validId: typing.Optional[str] = service.getValidId(idsList)
# Must be valid
if not validId:
raise Exception()
# Check secret if is stored
storedInfo : typing.Optional[typing.MutableMapping[str, typing.Any]] = service.recoverIdInfo(validId)
# If no secret valid
if not storedInfo or self._params['secret'] != storedInfo['secret']:
raise Exception()
# Notify Service that someone logged in/out
if login:
# Try to guess if this is a remote session
is_remote = self._params.get('session_type', '')[:3] in ('xrdp', 'RDP-')
service.processLogin(validId, remote_login=is_remote)
else:
service.processLogout(validId)
# All right, service notified...
except Exception:
raise BlockAccess()
class Login(LoginLogout):
"""
Notifies user logged id
"""
name = 'login'
def action(self) -> typing.MutableMapping[str, typing.Any]:
isManaged = self._params.get('type') != UNMANAGED
ip = hostname = ''
deadLine = maxIdle = None
logger.debug('Login Args: %s, Params: %s', self._args, self._params)
userService = self.getUserService()
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
osmanagers.OSManager.loggedIn(userService, self._params.get('username') or '')
maxIdle = osManager.maxIdle() if osManager else None
try:
userService: typing.Optional[UserService] = self.getUserService()
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
userService = None # Skip later processing userService
self.notifyService(login=True)
logger.debug('Max idle: %s', maxIdle)
if userService:
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
osmanagers.OSManager.loggedIn(userService, self._params.get('username') or '')
maxIdle = osManager.maxIdle() if osManager else None
logger.debug('Max idle: %s', maxIdle)
ip, hostname = userService.getConnectionSource()
deadLine = userService.deployed_service.getDeadline()
ip, hostname = userService.getConnectionSource()
deadLine = userService.deployed_service.getDeadline()
return ActorV3Action.actorResult({
'ip': ip,
'hostname': hostname,
@ -394,27 +456,39 @@ class Login(ActorV3Action):
'max_idle': maxIdle
})
class Logout(ActorV3Action):
class Logout(LoginLogout):
"""
Notifies user logged out
"""
name = 'logout'
def action(self) -> typing.MutableMapping[str, typing.Any]:
isManaged = self._params.get('type') != UNMANAGED
logger.debug('Args: %s, Params: %s', self._args, self._params)
userService = self.getUserService()
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
osmanagers.OSManager.loggedOut(userService, self._params.get('username') or '')
if osManager:
if osManager.isRemovableOnLogout(userService):
logger.debug('Removable on logout: %s', osManager)
try:
userService: typing.Optional[UserService] = self.getUserService()
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
userService = None # Skip later processing userService
self.notifyService(login=False) # Logout notification
if userService:
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
osmanagers.OSManager.loggedOut(userService, self._params.get('username') or '')
if osManager:
if osManager.isRemovableOnLogout(userService):
logger.debug('Removable on logout: %s', osManager)
userService.remove()
else:
userService.remove()
else:
userService.remove()
return ActorV3Action.actorResult('ok')
class Log(ActorV3Action):
"""
Sends a log from the service
@ -429,6 +503,7 @@ class Log(ActorV3Action):
return ActorV3Action.actorResult('ok')
class Ticket(ActorV3Action):
"""
Gets an stored ticket
@ -449,6 +524,7 @@ class Ticket(ActorV3Action):
except TicketStore.DoesNotExists:
return ActorV3Action.actorResult(error='Invalid ticket')
class Unmanaged(ActorV3Action):
name = 'unmanaged'
@ -502,6 +578,7 @@ class Unmanaged(ActorV3Action):
return ActorV3Action.actorResult(cert)
class Notify(ActorV3Action):
name = 'notify'
@ -529,4 +606,3 @@ class Notify(ActorV3Action):
incFailedIp(self._request.ip) # pylint: disable=protected-access
raise AccessDenied('Access denied')

View File

@ -70,11 +70,11 @@ class ContentProcessor:
return self._request.GET.copy()
def processParameters(self) -> typing.Any:
def processParameters(self) -> typing.MutableMapping[str, typing.Any]:
"""
Returns the parameter from the request
"""
return ''
return {}
def getResponse(self, obj):
"""
@ -119,7 +119,7 @@ class MarshallerProcessor(ContentProcessor):
"""
marshaller: typing.ClassVar[typing.Any] = None
def processParameters(self):
def processParameters(self) -> typing.MutableMapping[str, typing.Any]:
try:
if self._request.META.get('CONTENT_LENGTH', '0') == '0' or not self._request.body:
return self.processGetParameters()

View File

@ -293,6 +293,28 @@ class Service(Module):
def getValidId(self, idsList: typing.Iterable[str]) -> typing.Optional[str]:
return None
def processLogin(self, id: str, remote_login: bool) -> None:
"""
In the case that a login is invoked directly on an actor controlled machine with
an service token, this method will be called with provided info by uds actor (parameters)
Args:
id (str): Id validated through "getValidId"
remote_login (bool): if the login seems to be a remote login
"""
return
def processLogout(self, id: str) -> None:
"""
In the case that a login is invoked directly on an actor controlled machine with
an service token, this method will be called with provided info by uds actor (parameters)
Args:
id (str): Id validated through "getValidId"
remote_login (bool): if the login seems to be a remote login
"""
return
def storeIdInfo(self, id: str, data: typing.Any) -> None:
self.storage.putPickle('__nfo_' + id, data)