class ServiceTwo(Service): ''' Just a second service, no comments here (almost same that ServiceOne ''' typeName = _('Sample Service Two') typeType = 'SampleService2' typeDescription = _('Sample (and dummy) service ONE+ONE') iconFile = 'provider.png' #: We reuse provider icon here :-) # Functional related data maxDeployed = 5 usesCache = True cacheTooltip = _('L1 cache for dummy elements') usesCache_L2 = True cacheTooltip_L2 = _('L2 cache for dummy elements') needsManager = False mustAssignManually = False #: Types of publications. In this case, we will include a publication #: type for this one #: Note that this is a MUST if you indicate that needPublication publicationType = SamplePublication #: Types of deploys (services in cache and/or assigned to users) deployedType = SampleUserDeploymentTwo # Gui, we will use here the EditableList field names = gui.EditableList(label=_('List of names')) def __init__(self, environment, parent, values=None): ''' We here can get a HUGE list from client. Right now, this is treated same as other fields, in a near future we will se how to handle this better ''' super(ServiceTwo, self).__init__(environment, parent, values) # No checks here def getNames(self): ''' For using at deployed services, really nothing ''' return self.names.value
class IPMachinesService(IPServiceBase): # Gui ipList = gui.EditableList( label=_('List of servers'), tooltip=_('List of servers available for this service')) # Description of service typeName = _('Static Multiple IP') typeType = 'IPMachinesService' typeDescription = _( 'This service provides access to POWERED-ON Machines by IP') iconFile = 'machines.png' # Characteristics of service maxDeployed = -1 # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI, ) _ips: typing.List[str] def initialize(self, values: 'Module.ValuesType') -> None: if values is None or values.get('ipList', None) is None: self._ips = [] else: self._ips = list('{}~{}'.format(ip, i) for i, ip in enumerate( values['ipList'])) # Allow duplicates right now # self._ips.sort() def valuesDict(self) -> gui.ValuesDictType: ips = (i.split('~')[0] for i in self._ips) return {'ipList': gui.convertToList(ips)} def marshal(self) -> bytes: self.storage.saveData('ips', pickle.dumps(self._ips)) return b'v1' def unmarshal(self, data: bytes) -> None: if data == b'v1': d = self.storage.readData('ips') if isinstance(d, bytes): self._ips = pickle.loads(d) elif isinstance(d, str): # "legacy" saved elements self._ips = pickle.loads(d.encode('utf8')) self.marshal() # Ensure now is bytes.. else: self._ips = [] def getUnassignedMachine(self) -> typing.Optional[str]: # Search first unassigned machine try: for ip in self._ips: if self.storage.readData(ip) is None: self.storage.saveData(ip, ip) return ip return None except Exception: logger.exception("Exception at getUnassignedMachine") return None def unassignMachine(self, ip: str) -> None: try: self.storage.remove(ip) except Exception: logger.exception("Exception at getUnassignedMachine") def listAssignables(self): return [(ip, ip.split('~')[0]) for ip in self._ips if self.storage.readData(ip) is None] def assignFromAssignables( self, assignableId: str, user: '******', userDeployment: 'services.UserDeployment') -> str: userServiceInstance: IPMachineDeployed = typing.cast( IPMachineDeployed, userDeployment) if self.storage.readData(assignableId) is None: self.storage.saveData(assignableId, assignableId) return userServiceInstance.assign(assignableId) return userServiceInstance.error('IP already assigned')
class IPMachinesService(IPServiceBase): # Gui # Gui token = gui.TextField( order=1, label=_('Service Token'), length=16, tooltip=_('Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.'), defvalue='', required=False, rdonly=False ) ipList = gui.EditableList(label=_('List of servers'), tooltip=_('List of servers available for this service')) port = gui.NumericField(length=5, label=_('Check Port'), defvalue='0', order=2, tooltip=_('If non zero, only Machines responding to connection on that port will be used'), required=True, tab=gui.ADVANCED_TAB) # Description of service typeName = _('Static Multiple IP') typeType = 'IPMachinesService' typeDescription = _('This service provides access to POWERED-ON Machines by IP') iconFile = 'machines.png' # Characteristics of service maxDeployed = -1 # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI,) _ips: typing.List[str] = [] _token: str = '' _port: int = 0 def initialize(self, values: 'Module.ValuesType') -> None: if values is None: return if values.get('ipList', None) is None: self._ips = [] else: self._ips = ['{}~{}'.format(str(ip).strip(), i) for i, ip in enumerate(values['ipList']) if str(ip).strip()] # Allow duplicates right now # self._ips.sort() self._token = self.token.value.strip() self._port = self.port.value def getToken(self): return self._token or None def valuesDict(self) -> gui.ValuesDictType: ips = (i.split('~')[0] for i in self._ips) return {'ipList': gui.convertToList(ips), 'token': self._token, 'port': str(self._port)} def marshal(self) -> bytes: self.storage.saveData('ips', pickle.dumps(self._ips)) return b'\0'.join([b'v3', self._token.encode(), str(self._port).encode()]) def unmarshal(self, data: bytes) -> None: values: typing.List[bytes] = data.split(b'\0') d = self.storage.readData('ips') if isinstance(d, bytes): self._ips = pickle.loads(d) elif isinstance(d, str): # "legacy" saved elements self._ips = pickle.loads(d.encode('utf8')) self.marshal() # Ensure now is bytes.. else: self._ips = [] if values[0] != b'v1': self._token = values[1].decode() if values[0] == b'v3': self._port = int(values[2].decode()) def getUnassignedMachine(self) -> typing.Optional[str]: # Search first unassigned machine try: for ip in self._ips: theIP = ip.split('~')[0] if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) # Now, check if it is available on port, if required... if self._port > 0: if connection.testServer(theIP, self._port, timeOut=0.5) is False: # Log into logs of provider, so it can be "shown" on services logs self.parent().doLog(log.WARN, 'Host {} not accesible on port {}'.format(theIP, self._port)) self.storage.remove(theIP) # Return Machine to pool continue return theIP return None except Exception: logger.exception("Exception at getUnassignedMachine") return None def unassignMachine(self, ip: str) -> None: try: self.storage.remove(ip) except Exception: logger.exception("Exception at getUnassignedMachine") def listAssignables(self): return [(ip, ip.split('~')[0]) for ip in self._ips if self.storage.readData(ip) is None] def assignFromAssignables(self, assignableId: str, user: '******', userDeployment: 'services.UserDeployment') -> str: userServiceInstance: IPMachineDeployed = typing.cast(IPMachineDeployed, userDeployment) theIP = assignableId.split('~')[0] if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) return userServiceInstance.assign(theIP) return userServiceInstance.error('IP already assigned')
class IPMachinesService(IPServiceBase): # Gui token = gui.TextField( order=1, label=_('Service Token'), length=16, tooltip= _('Service token that will be used by actors to communicate with service. Leave empty for persistent assignation.' ), defvalue='', required=False, rdonly=False, ) ipList = gui.EditableList( label=_('List of servers'), tooltip=_('List of servers available for this service'), ) port = gui.NumericField( length=5, label=_('Check Port'), defvalue='0', order=2, tooltip= _('If non zero, only hosts responding to connection on that port will be served.' ), required=True, tab=gui.ADVANCED_TAB, ) skipTimeOnFailure = gui.NumericField( length=6, label=_('Skip time'), defvalue='0', order=2, tooltip=_( 'If a host fails to check, skip it for this time (in minutes).'), minValue=0, required=True, tab=gui.ADVANCED_TAB, ) # Description of service typeName = _('Static Multiple IP') typeType = 'IPMachinesService' typeDescription = _( 'This service provides access to POWERED-ON Machines by IP') iconFile = 'machines.png' # Characteristics of service maxDeployed = ( -1 ) # If the service provides more than 1 "provided service" (-1 = no limit, 0 = ???? (do not use it!!!), N = max number to deploy usesCache = False # Cache are running machine awaiting to be assigned usesCache_L2 = False # L2 Cache are running machines in suspended state needsManager = False # If the service needs a s.o. manager (managers are related to agents provided by services itselfs, i.e. virtual machines with agent) mustAssignManually = False # If true, the system can't do an automatic assignation of a deployed user service from this service deployedType = IPMachineDeployed servicesTypeProvided = (serviceTypes.VDI, ) _ips: typing.List[str] = [] _token: str = '' _port: int = 0 _skipTimeOnFailure: int = 0 def initialize(self, values: 'Module.ValuesType') -> None: if values is None: return if values.get('ipList', None) is None: self._ips = [] else: # Check that ips are valid for v in values['ipList']: if not net.isValidHost(v): raise IPServiceBase.ValidationException( gettext('Invalid value detected on servers list: "{}"' ).format(v)) self._ips = [ '{}~{}'.format(str(ip).strip(), i) for i, ip in enumerate(values['ipList']) if str(ip).strip() ] # Allow duplicates right now # Current stored data, if it exists d = self.storage.readData('ips') old_ips = pickle.loads(d) if d and isinstance(d, bytes) else [] # dissapeared ones dissapeared = set(i.split('~')[0] for i in old_ips) - set( i.split('~')[0] for i in self._ips) with transaction.atomic(): for removable in dissapeared: self.storage.remove(removable) self._token = self.token.value.strip() self._port = self.port.value self._skipTimeOnFailure = self.skipTimeOnFailure.num() def getToken(self): return self._token or None def valuesDict(self) -> gui.ValuesDictType: ips = (i.split('~')[0] for i in self._ips) return { 'ipList': gui.convertToList(ips), 'token': self._token, 'port': str(self._port), 'skipTimeOnFailure': str(self._skipTimeOnFailure), } def marshal(self) -> bytes: self.storage.saveData('ips', pickle.dumps(self._ips)) return b'\0'.join([ b'v4', self._token.encode(), str(self._port).encode(), str(self._skipTimeOnFailure).encode(), ]) def unmarshal(self, data: bytes) -> None: values: typing.List[bytes] = data.split(b'\0') d = self.storage.readData('ips') if isinstance(d, bytes): self._ips = pickle.loads(d) elif isinstance(d, str): # "legacy" saved elements self._ips = pickle.loads(d.encode('utf8')) self.marshal() # Ensure now is bytes.. else: self._ips = [] if values[0] != b'v1': self._token = values[1].decode() if values[0] in (b'v3', b'v4'): self._port = int(values[2].decode()) if values[0] == b'v4': self._skipTimeOnFailure = int(values[3].decode()) # Sets maximum services for this self.maxDeployed = len(self._ips) def getUnassignedMachine(self) -> typing.Optional[str]: # Search first unassigned machine try: now = getSqlDatetimeAsUnix() consideredFreeTime = now - config.GlobalConfig.SESSION_EXPIRE_TIME.getInt( force=False) * 3600 for ip in self._ips: theIP = IPServiceBase.getIp(ip) theMAC = IPServiceBase.getMac(ip) locked = self.storage.getPickle(theIP) if not locked or locked < consideredFreeTime: if self._port > 0 and self._skipTimeOnFailure > 0 and self.cache.get( 'port{}'.format(theIP)): continue # The check failed not so long ago, skip it... self.storage.putPickle(theIP, now) # Is WOL enabled? wolENABLED = bool(self.parent().wolURL(theIP, theMAC)) # Now, check if it is available on port, if required... if self._port > 0 and not wolENABLED: # If configured WOL, check is a nonsense if (connection.testServer( theIP, self._port, timeOut=0.5) is False): # Log into logs of provider, so it can be "shown" on services logs self.parent().doLog( log.WARN, 'Host {} not accesible on port {}'.format( theIP, self._port), ) logger.warning( 'Static Machine check on %s:%s failed. Will be ignored for %s minutes.', theIP, self._port, self._skipTimeOnFailure, ) self.storage.remove( theIP) # Return Machine to pool if self._skipTimeOnFailure > 0: self.cache.put( 'port{}'.format(theIP), '1', validity=self._skipTimeOnFailure * 60, ) continue if theMAC: return theIP + ';' + theMAC return theIP return None except Exception: logger.exception("Exception at getUnassignedMachine") return None def unassignMachine(self, ip: str) -> None: try: if ';' in ip: ip = ip.split(';')[0] # ; means that HAS an attached MAC self.storage.remove(ip) except Exception: logger.exception("Exception at getUnassignedMachine") def listAssignables(self): return [(ip, ip.split('~')[0]) for ip in self._ips if self.storage.readData(ip) is None] def assignFromAssignables( self, assignableId: str, user: '******', userDeployment: 'services.UserDeployment', ) -> str: userServiceInstance: IPMachineDeployed = typing.cast( IPMachineDeployed, userDeployment) theIP = IPServiceBase.getIp(assignableId) if self.storage.readData(theIP) is None: self.storage.saveData(theIP, theIP) return userServiceInstance.assign(theIP) return userServiceInstance.error('IP already assigned')
class SampleAuth(auths.Authenticator): """ This class represents a sample authenticator. As this, it will provide: * The authenticator functionality * 3 Groups, "Mortals", "Gods" and "Daemons", just random group names selected.. :-), plus groups that we enter at Authenticator form, from admin interface. * Search of groups (inside the 3 groups used in this sample plus entered) * Search for people (will return the search string + 000...999 as usernames) * The Required form description for administration interface, so admins can create new authenticators of this kind. In this sample, we will provide a simple standard auth, with owner drawn login form that will simply show users that has been created and allow web user to select one of them. For this class to get visible at administration client as a authenticator type, we MUST register it at package __init__ :note: At class level, the translations must be simply marked as so using ugettext_noop. This is done in this way because we will translate the string when it is sent to the administration client. """ # : Name of type, used at administration interface to identify this # : authenticator (i.e. LDAP, SAML, ...) # : This string will be translated when provided to admin interface # : using ugettext, so you can mark it as "_" at derived classes (using ugettext_noop) # : if you want so it can be translated. typeName = _('Sample Authenticator') # : Name of type used by Managers to identify this type of service # : We could have used here the Class name, but we decided that the # : module implementator will be the one that will provide a name that # : will relation the class (type) and that name. typeType = 'SampleAuthenticator' # : Description shown at administration level for this authenticator. # : This string will be translated when provided to admin interface # : using ugettext, so you can mark it as "_" at derived classes (using ugettext_noop) # : if you want so it can be translated. typeDescription = _('Sample dummy authenticator') # : Icon file, used to represent this authenticator at administration interface # : This file should be at same folder as this class is, except if you provide # : your own :py:meth:uds.core.module.BaseModule.icon method. iconFile = 'auth.png' # : Mark this authenticator as that the users comes from outside the UDS # : database, that are most authenticator (except Internal DB) # : True is the default value, so we do not need it in fact # isExternalSource = True # : If we need to enter the password for this user when creating a new # : user at administration interface. Used basically by internal authenticator. # : False is the default value, so this is not needed in fact # : needsPassword = False # : Label for username field, shown at administration interface user form. userNameLabel = _('Fake User') # Label for group field, shown at administration interface user form. groupNameLabel = _('Fake Group') # : Definition of this type of authenticator form # : We will define a simple form where we will use a simple # : list editor to allow entering a few group names groups = gui.EditableList(label=_('Groups'), values=['Gods', 'Daemons', 'Mortals']) def initialize( self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None: """ Simply check if we have at least one group in the list """ # To avoid problems, we only check data if values are passed # If values are not passed in, form data will only be available after # unserialization, and at this point all will be default values # so self.groups.value will be [] if values and len(self.groups.value) < 2: raise auths.Authenticator.ValidationException( _('We need more than two groups!')) def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: """ Here we will receive a pattern for searching users. This method is invoked from interface, so an administrator can search users. If we do not provide this method, the authenticator will not provide search facility for users. In our case, we will simply return a list of users (array of dictionaries with ids and names) with the pattern plus 1..10 """ return [{ 'id': '{0}-{1}'.format(pattern, a), 'name': '{0} number {1}'.format(pattern, a) } for a in range(1, 10)] def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]: """ Here we we will receive a patter for searching groups. In this sample, we will try to locate elements that where entered at sample authenticator form (when created), and return the ones that contains the pattern indicated. """ pattern = pattern.lower() res = [] for g in self.groups.value: if g.lower().find(pattern) != -1: res.append({'id': g, 'name': ''}) return res def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool: """ This method is invoked by UDS whenever it needs an user to be authenticated. It is used from web interface, but also from administration interface to check credentials and access of user. The tricky part of this method is the groupsManager, but it's easy to understand what is used it for. Imagine some authenticator, for example, an LDAP. It has its users, it has its groups, and it has it relations (which user belongs to which group). Now think about UDS. UDS know nothing about this, it only knows what the administator has entered at admin interface (groups mainly, but he can create users also). UDS knows about this groups, but we need to relation those with the ones know by the authenticator. To do this, we have created a simple mechanism, where the authenticator receives a groupsManager, that knows all groups known by UDS, and has the method so the authenticator can say, for the username being validated, to which uds groups it belongs to. This is done using the :py:meth:uds.core.auths.groups_manager.GroupsManager.validate method of the provided groups manager. At return, UDS will do two things: * If there is no group inside the groupsManager mareked as valid, it will denied access. * If there is some groups marked as valid, it will refresh the known UDS relations (this means that the database will be refresehd so the user has valid groups). This also means that the group membership is only checked at user login (well, in fact its also checked when an administrator tries to modify an user) So, authenticate must not also validate the user credentials, but also indicate the group membership of this user inside UDS. :note: groupsManager is an in/out parameter """ if username != credentials: # All users with same username and password are allowed return False # Now the tricky part. We will make this user belong to groups that contains at leat # two letters equals to the groups names known by UDS # For this, we will ask the groups manager for the groups names, and will check that and, # if the user match this criteria, will mark that group as valid for g in groupsManager.getGroupsNames(): if len(set(g.lower()).intersection(username.lower())) >= 2: groupsManager.validate(g) return True def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'): """ As with authenticator part related to groupsManager, this method will fill the groups to which the specified username belongs to. We have to fill up groupsManager from two different places, so it's not a bad idea to make a method that get the "real" authenticator groups and them simply call to :py:meth:uds.core.auths.groups_manager.GroupsManager.validate In our case, we simply repeat the process that we also do at authenticate """ for g in groupsManager.getGroupsNames(): if len(set(g.lower()).intersection(username.lower())) >= 2: groupsManager.validate(g) def getJavascript(self, request: 'HttpRequest') -> typing.Optional[str]: """ If we override this method from the base one, we are telling UDS that we want to draw our own authenticator. This way, we can do whataver we want here (for example redirect to a site for a single sign on) generation our ouwn html (and javascript ofc). """ # Here there is a sample, commented out # In this sample, we will make a list of valid users, and when clicked, # it will fill up original form with username and same password, and submit it. # res = '' # for u in self.dbAuthenticator().users.all(): # res += '<a class="myNames" id="{0}" href="">{0}</a><br/>'.format(u.name) # # res += '<script type="text/javascript">$(".myNames").click(function() { ' # res += '$("#id_user").val(this.id); $("#id_password").val(this.id); $("#loginform").submit(); return false;});</script>' # return res # I know, this is a bit ugly, but this is just a sample :-) res = '<p>Login name: <input id="logname" type="text"/></p>' res += '<p><a href="" onclick="window.location.replace(\'' + self.callbackUrl( ) + '?user='******'\' + $(\'#logname\').val()); return false;">Login</a></p>' return res def authCallback(self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager') -> typing.Optional[str]: """ We provide this as a sample of callback for an user. We will accept all petitions that has "user" parameter This method will get invoked by url redirections, probably by an SSO. The idea behind this is that we can provide: * Simple user/password authentications * Own authentications (not UDS, authenticator "owned"), but with no redirections * Own authentications via redirections (as most SSO will do) Here, we will receive the parameters for this """ user = parameters.get('user', None) return user def createUser(self, usrData: typing.Dict[str, str]) -> None: """ This method provides a "check oportunity" to authenticators for users created manually at administration interface. If we do not provide this method, the administration interface will not allow to create new users "by hand", i mean, the "new" options from menus will dissapear. usrData is a dictionary that contains the input parameters from user, with at least name, real_name, comments, state & password. We can modify this parameters, we can modify ALL, but name is not recommended to modify it unles you know what you are doing. Here, we will set the state to "Inactive" and realName to the same as username, but twice :-) """ from uds.core.util.state import State usrData['real_name'] = usrData['name'] + ' ' + usrData['name'] usrData['state'] = State.INACTIVE def modifyUser(self, usrData: typing.Dict[str, str]) -> None: """ This method provides a "check opportunity" to authenticator for users modified at administration interface. If we do not provide this method, nothing will happen (default one does nothing, but it's valid). usrData is a dictionary that contains the input parameters from user, with at least name, real_name, comments, state & password. We can modify this parameters, we can modify ALL, but name is not recommended to modify it unless you know what you are doing. Here, we will simply update the realName of the user, and (we have to take care this this kind of things) modify the userName to a new one, the original plus '-1' """ usrData['real_name'] = usrData['name'] + ' ' + usrData['name'] usrData['name'] += '-1'