mirror of
https://github.com/dkmstr/openuds.git
synced 2025-03-12 04:58:34 +03:00
working on allowing services to detect user login on unmanaged services
This commit is contained in:
parent
00dc4c5a7b
commit
74ad50d7d8
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
if self._cfg.own_token:
|
||||
result = self._api.login(self._cfg.own_token, username, sessionType)
|
||||
master_token = self._cfg.master_token
|
||||
secret = self._secret
|
||||
|
||||
# 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)
|
||||
|
||||
|
@ -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
|
||||
@ -309,7 +319,7 @@ class BaseReadyChange(ActorV3Action):
|
||||
osManager.toReady(userService)
|
||||
userServiceManager().notifyReadyFromOsManager(userService, '')
|
||||
|
||||
# Generates a certificate and send it to client.
|
||||
# Generates a certificate and send it to client.
|
||||
privateKey, cert, password = certs.selfSignedCert(self._params['ip'])
|
||||
# Store certificate with userService
|
||||
userService.setProperty('cert', cert)
|
||||
@ -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')
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user