def symlink(self, target_path, path): """Handle operations of same name""" target_path = force_utf8(target_path) path = force_utf8(path) self.logger.debug('symlink %s %s' % (target_path, path)) # Prevent users from creating symlinks for security reasons self.logger.error("symlink rejected on path %s :: %s" % (target_path, path)) return paramiko.SFTP_OP_UNSUPPORTED
def rename(self, oldpath, newpath): """Handle operations of same name""" oldpath = force_utf8(oldpath) newpath = force_utf8(newpath) self.logger.debug("rename %s %s" % (oldpath, newpath)) try: real_oldpath = self._get_fs_path(oldpath) except ValueError, err: self.logger.warning('rename %s %s: %s' % (oldpath, newpath, err)) return paramiko.SFTP_PERMISSION_DENIED
def warn_on_rejects(rejects, output_objects): """Helper to fill in output_objects in case of rejects""" if rejects: for (key, err_list) in rejects.items(): for err in err_list: output_objects.append({ 'object_type': 'error_text', 'text': 'input parsing error: %s: %s: %s' % (key, force_utf8(err[0]), force_utf8(err[1])) })
def list_country_codes(configuration): """Get a sorted list of available countries and their 2-letter ISO3166 country code for use in country selection during account sign up. """ logger = configuration.logger if iso3166 is None: logger.info("iso3166 module not available - manual country code entry") return False country_list = [] for entry in iso3166.countries: name, code = force_utf8(entry.name), force_utf8(entry.alpha2) logger.debug("found country %s for code %s" % (name, code)) country_list.append((name, code)) return country_list
def send_email( recipients, subject, message, logger, configuration, ): """Send message to recipients by email: Force utf8 encoding to avoid accented characters appearing garbled """ recipients_list = recipients.split(', ') try: mime_msg = MIMEText(force_utf8(message), "plain", "utf8") mime_msg['Subject'] = subject mime_msg['From'] = configuration.smtp_sender mime_msg['To'] = recipients server = smtplib.SMTP(configuration.smtp_server) server.set_debuglevel(0) errors = server.sendmail(configuration.smtp_sender, recipients_list, mime_msg.as_string()) server.quit() if errors: logger.warning('Partial error(s) sending email: %s' % errors) return False else: logger.debug('Email was sent to %s' % recipients) return True except Exception, err: logger.error('Sending email to %s through %s failed!: %s' % (recipients, configuration.smtp_server, str(err))) return False
def __str__(self): """Byte string formater - username is already forced to utf8 so other strings are converted here as well. """ out = '''username: %s home: %s''' % (self.username, self.home) if self.password: out += ''' password: %s''' % force_utf8(self.password) if self.digest: out += ''' digest: %s''' % force_utf8(self.digest) if self.public_key: out += ''' pubkey: %s''' % force_utf8(self.public_key.get_base64()) out += ''' last_update: %s''' % self.last_update return out
def readlink(self, path): """Handle operations of same name""" path = force_utf8(path) self.logger.debug("readlink %s" % path) try: real_path = self._get_fs_path(path) except ValueError, err: self.logger.warning('readlink %s: %s' % (path, err)) return paramiko.SFTP_PERMISSION_DENIED
def _chmod(self, path, mode, sftphandle=None): """Handle chmod for SimpleSftpServer and SFTPHandle""" file_obj = None path = force_utf8(path) self.logger.debug("_chmod %s" % path) try: real_path = self._get_fs_path(path) except ValueError, err: self.logger.warning('chmod %s: %s' % (path, err)) return paramiko.SFTP_PERMISSION_DENIED
def __update_crontab_monitor( self, configuration, src_path, state, ): pid = multiprocessing.current_process().pid if state == 'created': # logger.debug('(%s) Updating crontab monitor for src_path: %s, event: %s' # % (pid, src_path, state)) print '(%s) Updating crontab monitor for src_path: %s, event: %s' \ % (pid, src_path, state) if os.path.exists(src_path): # _crontab_monitor_lock.acquire() if not shared_state['crontab_inotify']._wd_for_path.has_key(src_path): # logger.debug('(%s) Adding watch for: %s' % (pid, # src_path)) shared_state['crontab_inotify'].add_watch( force_utf8(src_path)) # Fire 'modified' events for all dirs and files in subpath # to ensure that all crontab files are loaded for ent in scandir(src_path): if ent.is_dir(follow_symlinks=True): # logger.debug('(%s) Dispatch DirCreatedEvent for: %s' # % (pid, ent.path)) shared_state['crontab_handler'].dispatch( DirCreatedEvent(ent.path)) elif ent.path.find(configuration.user_settings) \ > -1: # logger.debug('(%s) Dispatch FileCreatedEvent for: %s' # % (pid, ent.path)) shared_state['crontab_handler'].dispatch( FileCreatedEvent(ent.path)) # else: # logger.debug('(%s) crontab_monitor watch already exists for: %s' # % (pid, src_path)) else: logger.debug('(%s) unhandled event: %s for: %s' % (pid, state, src_path))
def generate_ssh_rsa_key_pair(size=2048, public_key_prefix='', public_key_postfix='', encode_utf8=False): """Generates ssh rsa key pair""" if paramiko is None: raise Exception("You need paramiko to provide the ssh/sftp service") rsa_key = paramiko.RSAKey.generate(size) string_io_obj = StringIO.StringIO() rsa_key.write_private_key(string_io_obj) private_key = string_io_obj.getvalue() public_key = ( "%s ssh-rsa %s %s" % (public_key_prefix, rsa_key.get_base64(), public_key_postfix)).strip() if encode_utf8: private_key = force_utf8(private_key) public_key = force_utf8(public_key) return (private_key, public_key)
def get_twofactor_token(configuration, client_id, b32_key): """Get current twofactor taken for base32 key""" _logger = configuration.logger if pyotp is None: raise Exception("The pyotp module is missing and required for 2FA") if configuration.site_enable_gdp: client_id = get_base_client_id(configuration, client_id, expand_oid_alias=False) # IMPORTANT: pyotp unicode breaks when used in our strings - force utf8! totp = get_totp(client_id, b32_key, configuration) token = totp.now() token = force_utf8(token) return token
def get_twofactor_secrets(configuration, client_id): """Load twofactor base32 key and OTP uri for QR code. Generates secret for user if not already done. Actual twofactor login requirement is not enabled here, however. """ _logger = configuration.logger if pyotp is None: raise Exception("The pyotp module is missing and required for 2FA") if configuration.site_enable_gdp: client_id = get_base_client_id(configuration, client_id, expand_oid_alias=False) # NOTE: 2FA secret key is a standalone file in user settings dir # Try to load existing and generate new one if not there. # We need the base32-encoded form as returned here. b32_key = load_twofactor_key(client_id, configuration) if not b32_key: b32_key = reset_twofactor_key(client_id, configuration) totp = get_totp(client_id, b32_key, configuration) # URI-format for otp auth is # otpauth://<otptype>/(<issuer>:)<accountnospaces>? # secret=<secret>(&issuer=<issuer>)(&image=<imageuri>) # which we pull out of pyotp directly. # We could display with Google Charts helper like this example # https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr& # chl=otpauth://totp/Example:[email protected]? # secret=JBSWY3DPEHPK3PXP&issuer=Example # but we prefer to use the QRious JS library to keep it local. if configuration.user_openid_alias: username = extract_field(client_id, configuration.user_openid_alias) else: username = client_id otp_uri = totp.provisioning_uri(username, issuer_name=configuration.short_title) # IMPORTANT: pyotp unicode breaks wsgi when inserted - force utf8! otp_uri = force_utf8(otp_uri) # Google img examle # img_url = 'https://www.google.com/chart?' # img_url += urllib.urlencode([('cht', 'qr'), ('chld', 'M|0'), # ('chs', '200x200'), ('chl', otp_uri)]) # otp_img = '<img src="%s" />' % img_url return (b32_key, totp.interval, otp_uri)
def reset_twofactor_key(client_id, configuration, seed=None, interval=None): """Reset 2FA secret key and write to user settings file in scrambled form. Return the new secret key on unscrambled base32 form. """ _logger = configuration.logger if configuration.site_enable_gdp: client_id = get_base_client_id(configuration, client_id, expand_oid_alias=False) client_dir = client_id_dir(client_id) key_path = os.path.join(configuration.user_settings, client_dir, twofactor_key_name) try: if pyotp is None: raise Exception("The pyotp module is missing and required for 2FA") if not seed: b32_key = pyotp.random_base32(length=twofactor_key_bytes) else: b32_key = seed # NOTE: pyotp.random_base32 returns unicode # which causes trouble with WSGI b32_key = force_utf8(b32_key) scrambled = scramble_password(configuration.site_password_salt, b32_key) key_fd = open(key_path, 'w') key_fd.write(scrambled) key_fd.close() # Reset interval interval_path = os.path.join(configuration.user_settings, client_dir, twofactor_interval_name) delete_file(interval_path, _logger, allow_missing=True) if interval: i_fd = open(interval_path, 'w') i_fd.write("%d" % interval) i_fd.close() except Exception, exc: _logger.error("failed in reset 2FA key: %s" % exc) return False
def handle_package_upload( real_src, relative_src, client_id, configuration, submit_mrslfiles, dst, ): """A file package was uploaded (eg. .zip file). Extract the content and submit mrsl files if submit_mrsl_files is True. """ logger = configuration.logger msg = '' status = True logger.info("handle_package_upload %s %s %s" % \ (real_src, relative_src, dst)) client_dir = client_id_dir(client_id) # Please note that base_dir must end in slash to avoid access to other # user dirs when own name is a prefix of another user name base_dir = os.path.abspath(os.path.join(configuration.user_home, client_dir)) + os.sep # Unpack in same directory unless real_dst is given if not dst: real_dst = os.path.abspath(os.path.dirname(real_src)) elif os.path.isabs(dst): real_dst = os.path.abspath(dst) else: real_dst = os.path.join(base_dir, dst) real_dst += os.sep mrslfiles_to_parse = [] real_src_lower = real_src.lower() if real_src_lower.endswith('.zip'): # Handle .zip file msg += "Received '%s' for unpacking. " % relative_src try: zip_object = zipfile.ZipFile(real_src, 'r', allowZip64=True) except Exception, exc: logger.error("open zip failed: %s" % exc) msg += 'Could not open zipfile: %s! ' % exc return (False, msg) logger.info("unpack entries of %s to %s" % \ (real_src, real_dst)) for zip_entry in zip_object.infolist(): entry_filename = force_utf8(zip_entry.filename) msg += 'Extracting: %s . ' % entry_filename # write zip_entry to disk # IMPORTANT: we must abs-expand for valid_user_path_name check # otherwise it will incorrectly fail on e.g. abc/ # dir entry in archive local_zip_entry_name = os.path.join(real_dst, entry_filename) valid_status, valid_err = valid_user_path_name( entry_filename, os.path.abspath(local_zip_entry_name), base_dir) if not valid_status: status = False msg += "Filename validation error: %s! " % valid_err continue # create sub dir(s) if missing zip_entry_dir = os.path.dirname(local_zip_entry_name) if not os.path.isdir(zip_entry_dir): msg += 'Creating dir %s . ' % entry_filename try: os.makedirs(zip_entry_dir, 0775) except Exception, exc: logger.error("create directory failed: %s" % exc) msg += 'Error creating directory: %s! ' % exc status = False continue if os.path.isdir(local_zip_entry_name): logger.debug("nothing more to do for dir entry: %s" % \ local_zip_entry_name) continue try: zip_data = zip_object.read(zip_entry.filename) except Exception, exc: logger.error("read data in %s failed: %s" % \ (zip_entry.filename, exc)) msg += 'Error reading %s :: %s! ' % (zip_entry.filename, exc) status = False continue
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False, op_menu=False) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) if not correct_handler('POST'): output_objects.append( {'object_type': 'error_text', 'text' : 'Only accepting POST requests to prevent unintended updates'}) return (output_objects, returnvalues.CLIENT_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = '%s certificate request' % configuration.short_title title_entry['skipmenu'] = True output_objects.append({'object_type': 'header', 'text' : '%s certificate request' % \ configuration.short_title }) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. raw_name = accepted['cert_name'][-1].strip() try: cert_name = force_utf8(force_unicode(raw_name).title()) except Exception: cert_name = raw_name.title() country = accepted['country'][-1].strip().upper() state = accepted['state'][-1].strip().title() org = accepted['org'][-1].strip() # lower case email address email = accepted['email'][-1].strip().lower() password = accepted['password'][-1] verifypassword = accepted['verifypassword'][-1] # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') if password != verifypassword: output_objects.append({'object_type': 'error_text', 'text' : 'Password and verify password are not identical!' }) return (output_objects, returnvalues.CLIENT_ERROR) # TODO: move this check to conf? if not forced_org_email_match(org, email, configuration): output_objects.append({'object_type': 'error_text', 'text' : '''Illegal email and organization combination: Please read and follow the instructions in red on the request page! If you are a student with only a @*.ku.dk address please just use KU as organization. As long as you state that you want the certificate for course purposes in the comment field, you will be given access to the necessary resources anyway. '''}) return (output_objects, returnvalues.CLIENT_ERROR) user_dict = { 'full_name': cert_name, 'organization': org, 'state': state, 'country': country, 'email': email, 'comment': comment, 'password': base64.b64encode(password), 'expire': int(time.time() + cert_valid_days * 24 * 60 * 60), 'openid_names': [], } fill_distinguished_name(user_dict) user_id = user_dict['distinguished_name'] user_dict['authorized'] = (user_id == client_id) if configuration.user_openid_providers and configuration.user_openid_alias: user_dict['openid_names'] += \ [user_dict[configuration.user_openid_alias]] logger.info('got reqcert request: %s' % user_dict) # For testing only if cert_name.upper().find('DO NOT SEND') != -1: output_objects.append({'object_type': 'text', 'text' : "Test request ignored!"}) return (output_objects, returnvalues.OK) req_path = None try: (os_fd, req_path) = tempfile.mkstemp(dir=user_pending) os.write(os_fd, dumps(user_dict)) os.close(os_fd) except Exception, err: logger.error('Failed to write certificate request to %s: %s' % (req_path, err)) output_objects.append({'object_type': 'error_text', 'text' : 'Request could not be sent to grid administrators. Please contact them manually on %s if this error persists.' % admin_email}) return (output_objects, returnvalues.SYSTEM_ERROR)
logger.error("open tar bz failed: %s" % exc) msg += 'Could not open .tar.bz2 file: %s! ' % exc return (False, msg) else: try: tar_object = tarfile.open(real_src, 'r') tar_file_content = tarfile.TarFile.open(real_src) except Exception, exc: logger.error("open tar failed: %s" % exc) msg += 'Could not open .tar file: %s! ' % exc return (False, msg) logger.info("unpack entries of %s to %s" % \ (real_src, real_dst)) for tar_entry in tar_object: entry_filename = force_utf8(tar_entry.name) msg += 'Extracting: %s . ' % entry_filename # write tar_entry to disk # IMPORTANT: we must abs-expand for valid_user_path_name check # otherwise it will incorrectly fail on e.g. abc/ # dir entry in archive local_tar_entry_name = os.path.join(real_dst, entry_filename) valid_status, valid_err = valid_user_path_name( entry_filename, os.path.abspath(local_tar_entry_name), base_dir) if not valid_status: status = False msg += "Filename validation error: %s! " % valid_err continue
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) output_objects.append({'object_type': 'header', 'text' : '%s external certificate sign up' % \ configuration.short_title }) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert( user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, require_user=False ) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) if not correct_handler('POST'): output_objects.append( {'object_type': 'error_text', 'text' : 'Only accepting POST requests to prevent unintended updates'}) return (output_objects, returnvalues.CLIENT_ERROR) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) cert_id = accepted['cert_id'][-1].strip() # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. raw_name = accepted['cert_name'][-1].strip() try: cert_name = force_utf8(force_unicode(raw_name).title()) except Exception: cert_name = raw_name.title() country = accepted['country'][-1].strip().upper() state = accepted['state'][-1].strip().title() org = accepted['org'][-1].strip() # lower case email address email = accepted['email'][-1].strip().lower() # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') is_diku_email = False is_diku_org = False if email.find('@diku.dk') != -1: is_diku_email = True if 'DIKU' == org.upper(): # Consistent upper casing org = org.upper() is_diku_org = True if is_diku_org != is_diku_email: output_objects.append({'object_type': 'error_text', 'text' : '''Illegal email and organization combination: Please read and follow the instructions in red on the request page! If you are a DIKU student with only a @*.ku.dk address please just use KU as organization. As long as you state that you want the certificate for DIKU purposes in the comment field, you will be given access to the necessary resources anyway. '''}) return (output_objects, returnvalues.CLIENT_ERROR) try: distinguished_name_to_user(cert_id) except: output_objects.append({'object_type': 'error_text', 'text' : '''Illegal Distinguished name: Please note that the distinguished name must be a valid certificate DN with multiple "key=val" fields separated by "/". '''}) return (output_objects, returnvalues.CLIENT_ERROR) user_dict = { 'distinguished_name': cert_id, 'full_name': cert_name, 'organization': org, 'state': state, 'country': country, 'email': email, 'password': '', 'comment': '%s: %s' % ('Existing certificate', comment), 'expire': int(time.time() + cert_valid_days * 24 * 60 * 60), 'openid_names': [], } fill_distinguished_name(user_dict) user_id = user_dict['distinguished_name'] if configuration.user_openid_providers and configuration.user_openid_alias: user_dict['openid_names'] += \ [user_dict[configuration.user_openid_alias]] logger.info('got extcert request: %s' % user_dict) # If server allows automatic addition of users with a CA validated cert # we create the user immediately and skip mail if configuration.auto_add_cert_user: fill_user(user_dict) # Now all user fields are set and we can begin adding the user db_path = os.path.join(configuration.mig_server_home, user_db_filename) try: create_user(user_dict, configuration.config_file, db_path, ask_renew=False) except Exception, err: logger.error('Failed to create user with existing cert %s: %s' % (cert_id, err)) output_objects.append( {'object_type': 'error_text', 'text' : '''Could not create the user account for you: Please report this problem to the grid administrators (%s).''' % \ admin_email}) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({'object_type': 'text', 'text' : '''Created the user account for you: Please use the navigation menu to the left to proceed using it. '''}) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False) output_objects.append({'object_type': 'header', 'text' : '%s external certificate sign up' % \ configuration.short_title }) defaults = signature()[1] (validate_status, accepted) = validate_input_and_cert(user_arguments_dict, defaults, output_objects, client_id, configuration, allow_rejects=False, require_user=False) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) cert_id = accepted['cert_id'][-1].strip() # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. raw_name = accepted['cert_name'][-1].strip() try: cert_name = force_utf8(force_unicode(raw_name).title()) except Exception: cert_name = raw_name.title() country = accepted['country'][-1].strip().upper() state = accepted['state'][-1].strip().title() org = accepted['org'][-1].strip() # lower case email address email = accepted['email'][-1].strip().lower() # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) is_diku_email = False is_diku_org = False if email.find('@diku.dk') != -1: is_diku_email = True if 'DIKU' == org.upper(): # Consistent upper casing org = org.upper() is_diku_org = True if is_diku_org != is_diku_email: output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal email and organization combination: Please read and follow the instructions in red on the request page! If you are a DIKU student with only a @*.ku.dk address please just use KU as organization. As long as you state that you want the certificate for DIKU purposes in the comment field, you will be given access to the necessary resources anyway. ''' }) return (output_objects, returnvalues.CLIENT_ERROR) try: distinguished_name_to_user(cert_id) except: output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal Distinguished name: Please note that the distinguished name must be a valid certificate DN with multiple "key=val" fields separated by "/". ''' }) return (output_objects, returnvalues.CLIENT_ERROR) user_dict = { 'distinguished_name': cert_id, 'full_name': cert_name, 'organization': org, 'state': state, 'country': country, 'email': email, 'password': '', 'comment': '%s: %s' % ('Existing certificate', comment), 'expire': int(time.time() + cert_valid_days * 24 * 60 * 60), 'openid_names': [], 'auth': ['extcert'], } fill_distinguished_name(user_dict) user_id = user_dict['distinguished_name'] if configuration.user_openid_providers and configuration.user_openid_alias: user_dict['openid_names'] += \ [user_dict[configuration.user_openid_alias]] logger.info('got extcert request: %s' % user_dict) # If server allows automatic addition of users with a CA validated cert # we create the user immediately and skip mail if configuration.auto_add_cert_user: fill_user(user_dict) # Now all user fields are set and we can begin adding the user db_path = os.path.join(configuration.mig_server_home, user_db_filename) try: create_user(user_dict, configuration.config_file, db_path, ask_renew=False) except Exception, err: logger.error('Failed to create user with existing cert %s: %s' % (cert_id, err)) output_objects.append( {'object_type': 'error_text', 'text' : '''Could not create the user account for you: Please report this problem to the grid administrators (%s).''' % \ admin_email}) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({ 'object_type': 'text', 'text': '''Created the user account for you: Please use the navigation menu to the left to proceed using it. ''' }) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict, environ=None): """Main function used by front end""" if environ is None: environ = os.environ (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False, op_menu=False) logger = configuration.logger logger.info('%s: args: %s' % (op_name, user_arguments_dict)) prefilter_map = {} output_objects.append({ 'object_type': 'header', 'text': 'Automatic %s sign up' % configuration.short_title }) (_, identity) = extract_client_openid(configuration, environ, lookup_dn=False) req_url = environ['SCRIPT_URI'] if client_id and client_id == identity: login_type = 'cert' if req_url.startswith(configuration.migserver_https_mig_cert_url): base_url = configuration.migserver_https_mig_cert_url elif req_url.startswith(configuration.migserver_https_ext_cert_url): base_url = configuration.migserver_https_ext_cert_url else: logger.warning('no match for cert request URL: %s' % req_url) output_objects.append({ 'object_type': 'error_text', 'text': 'No matching request URL: %s' % req_url }) return (output_objects, returnvalues.SYSTEM_ERROR) elif identity: login_type = 'oid' if req_url.startswith(configuration.migserver_https_mig_oid_url): base_url = configuration.migserver_https_mig_oid_url elif req_url.startswith(configuration.migserver_https_ext_oid_url): base_url = configuration.migserver_https_ext_oid_url else: logger.warning('no match for oid request URL: %s' % req_url) output_objects.append({ 'object_type': 'error_text', 'text': 'No matching request URL: %s' % req_url }) return (output_objects, returnvalues.SYSTEM_ERROR) for name in ('openid.sreg.cn', 'openid.sreg.fullname', 'openid.sreg.full_name'): prefilter_map[name] = filter_commonname else: output_objects.append({ 'object_type': 'error_text', 'text': 'Missing user credentials' }) return (output_objects, returnvalues.CLIENT_ERROR) defaults = signature(login_type)[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False, prefilter_map=prefilter_map) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) logger.debug('Accepted arguments: %s' % accepted) # Unfortunately OpenID redirect does not use POST if login_type != 'oid' and not safe_handler( configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append({ 'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) admin_email = configuration.admin_email (openid_names, oid_extras) = ([], {}) # Extract raw values if login_type == 'cert': uniq_id = accepted['cert_id'][-1].strip() raw_name = accepted['cert_name'][-1].strip() country = accepted['country'][-1].strip() state = accepted['state'][-1].strip() org = accepted['org'][-1].strip() org_unit = '' role = ','.join([i for i in accepted['role'] if i]) association = ','.join([i for i in accepted['association'] if i]) locality = '' timezone = '' email = accepted['email'][-1].strip() raw_login = None elif login_type == 'oid': uniq_id = accepted['openid.sreg.nickname'][-1].strip() \ or accepted['openid.sreg.short_id'][-1].strip() raw_name = accepted['openid.sreg.fullname'][-1].strip() \ or accepted['openid.sreg.full_name'][-1].strip() country = accepted['openid.sreg.country'][-1].strip() state = accepted['openid.sreg.state'][-1].strip() org = accepted['openid.sreg.o'][-1].strip() \ or accepted['openid.sreg.organization'][-1].strip() org_unit = accepted['openid.sreg.ou'][-1].strip() \ or accepted['openid.sreg.organizational_unit'][-1].strip() # We may receive multiple roles and associations role = ','.join([i for i in accepted['openid.sreg.role'] if i]) association = ','.join( [i for i in accepted['openid.sreg.association'] if i]) locality = accepted['openid.sreg.locality'][-1].strip() timezone = accepted['openid.sreg.timezone'][-1].strip() # We may encounter results without an email, fall back to uniq_id then email = accepted['openid.sreg.email'][-1].strip() or uniq_id # Fix case of values: # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. try: full_name = force_utf8(force_unicode(raw_name).title()) except Exception: logger.warning('could not use unicode form to capitalize full name') full_name = raw_name.title() country = country.upper() state = state.upper() email = email.lower() if login_type == 'oid': # Remap some oid attributes if on KIT format with faculty in # organization and institute in organizational_unit. We can add them # as different fields as long as we make sure the x509 fields are # preserved. # Additionally in the special case with unknown institute (ou=ukendt) # we force organization to KU to align with cert policies. # We do that to allow autocreate updating existing cert users. if org_unit not in ('', 'NA'): org_unit = org_unit.upper() oid_extras['faculty'] = org oid_extras['institute'] = org_unit org = org_unit.upper() org_unit = 'NA' if org == 'UKENDT': org = 'KU' logger.info('unknown affilition, set organization to %s' % org) # Stay on virtual host - extra useful while we test dual OpenID if configuration.site_enable_gdp: base_url = environ.get('REQUEST_URI', base_url).split('?')[0].replace( 'autocreate', 'gdpman') else: base_url = environ.get('REQUEST_URI', base_url).split('?')[0].replace( 'autocreate', 'fileman') raw_login = None for oid_provider in configuration.user_openid_providers: openid_prefix = oid_provider.rstrip('/') + '/' if identity.startswith(openid_prefix): raw_login = identity.replace(openid_prefix, '') break if raw_login: openid_names.append(raw_login) # we should have the proxy file read... proxy_content = accepted['proxy_upload'][-1] # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') user_dict = { 'short_id': uniq_id, 'full_name': full_name, 'organization': org, 'organizational_unit': org_unit, 'locality': locality, 'state': state, 'country': country, 'email': email, 'role': role, 'association': association, 'timezone': timezone, 'password': '', 'comment': '%s: %s' % ('Existing certificate', comment), 'openid_names': openid_names, } user_dict.update(oid_extras) # We must receive some ID from the provider if not uniq_id and not email: if accepted.get('openid.sreg.required', '') and identity: output_objects.append({ 'object_type': 'html_form', 'text': '''<p class="spinner iconleftpad"> Auto log out first to avoid sign up problems ... </p>''' }) html = \ """ <a id='autologout' href='%s'></a> <script type='text/javascript'> document.getElementById('autologout').click(); </script>""" \ % openid_autologout_url(configuration, identity, client_id, req_url, user_arguments_dict) output_objects.append({'object_type': 'html_form', 'text': html}) return (output_objects, returnvalues.CLIENT_ERROR) auth = 'unknown' if login_type == 'cert': auth = 'extcert' user_dict['expire'] = int(time.time() + cert_valid_days * 24 * 60 * 60) try: distinguished_name_to_user(uniq_id) user_dict['distinguished_name'] = uniq_id except: output_objects.append({ 'object_type': 'error_text', 'text': '''Illegal Distinguished name: Please note that the distinguished name must be a valid certificate DN with multiple "key=val" fields separated by "/". ''' }) return (output_objects, returnvalues.CLIENT_ERROR) elif login_type == 'oid': auth = 'extoid' user_dict['expire'] = int(time.time() + oid_valid_days * 24 * 60 * 60) fill_distinguished_name(user_dict) uniq_id = user_dict['distinguished_name'] # Save auth access method user_dict['auth'] = [auth] # If server allows automatic addition of users with a CA validated cert # we create the user immediately and skip mail if login_type == 'cert' and configuration.auto_add_cert_user \ or login_type == 'oid' and configuration.auto_add_oid_user: fill_user(user_dict) logger.info('create user: %s' % user_dict) # Now all user fields are set and we can begin adding the user db_path = os.path.join(configuration.mig_server_home, user_db_filename) try: create_user(user_dict, configuration.config_file, db_path, ask_renew=False, default_renew=True) if configuration.site_enable_griddk \ and accepted['proxy_upload'] != ['']: # save the file, display expiration date proxy_out = handle_proxy(proxy_content, uniq_id, configuration) output_objects.extend(proxy_out) except Exception, err: logger.error('create failed for %s: %s' % (uniq_id, err)) output_objects.append({ 'object_type': 'error_text', 'text': '''Could not create the user account for you: Please report this problem to the grid administrators (%s).''' % admin_email }) return (output_objects, returnvalues.SYSTEM_ERROR) logger.info('created user account for %s' % uniq_id) output_objects.append({ 'object_type': 'html_form', 'text': '''Created the user account for you - please open <a href="%s">your personal page</a> to proceed using it. ''' % base_url }) return (output_objects, returnvalues.OK)
def main(client_id, user_arguments_dict, environ=None): """Main function used by front end""" if environ is None: environ = os.environ (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False, op_menu=False) logger = configuration.logger logger.info('%s: args: %s' % (op_name, user_arguments_dict)) prefilter_map = {} output_objects.append({'object_type': 'header', 'text' : 'Automatic %s sign up' % \ configuration.short_title }) identity = extract_client_openid(configuration, environ, lookup_dn=False) if client_id and client_id == identity: login_type = 'cert' base_url = configuration.migserver_https_cert_url elif identity: login_type = 'oid' base_url = configuration.migserver_https_oid_url for name in ('openid.sreg.cn', 'openid.sreg.fullname', 'openid.sreg.full_name'): prefilter_map[name] = filter_commonname else: output_objects.append( {'object_type': 'error_text', 'text': 'Missing user credentials'}) return (output_objects, returnvalues.CLIENT_ERROR) defaults = signature(login_type)[1] (validate_status, accepted) = validate_input( user_arguments_dict, defaults, output_objects, allow_rejects=False, prefilter_map=prefilter_map) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) logger.debug('Accepted arguments: %s' % accepted) # Unfortunately OpenID redirect does not use POST if login_type != 'oid' and not correct_handler('POST'): output_objects.append( {'object_type': 'error_text', 'text' : 'Only accepting POST requests to prevent unintended updates'}) return (output_objects, returnvalues.CLIENT_ERROR) admin_email = configuration.admin_email openid_names, oid_extras = [], {} # Extract raw values if login_type == 'cert': uniq_id = accepted['cert_id'][-1].strip() raw_name = accepted['cert_name'][-1].strip() country = accepted['country'][-1].strip() state = accepted['state'][-1].strip() org = accepted['org'][-1].strip() org_unit = '' role = ','.join([i for i in accepted['role'] if i]) locality = '' timezone = '' email = accepted['email'][-1].strip() raw_login = None elif login_type == 'oid': uniq_id = accepted['openid.sreg.nickname'][-1].strip() or \ accepted['openid.sreg.short_id'][-1].strip() raw_name = accepted['openid.sreg.fullname'][-1].strip() or \ accepted['openid.sreg.full_name'][-1].strip() country = accepted['openid.sreg.country'][-1].strip() state = accepted['openid.sreg.state'][-1].strip() org = accepted['openid.sreg.o'][-1].strip() or \ accepted['openid.sreg.organization'][-1].strip() org_unit = accepted['openid.sreg.ou'][-1].strip() or \ accepted['openid.sreg.organizational_unit'][-1].strip() # We may receive multiple roles role = ','.join([i for i in accepted['openid.sreg.role'] if i]) locality = accepted['openid.sreg.locality'][-1].strip() timezone = accepted['openid.sreg.timezone'][-1].strip() email = accepted['openid.sreg.email'][-1].strip() # Fix case of values: # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. try: full_name = force_utf8(force_unicode(raw_name).title()) except Exception: logger.warning("could not use unicode form to capitalize full name") full_name = raw_name.title() country = country.upper() state = state.upper() email = email.lower() if login_type == 'oid': # Remap some oid attributes if on kit format with faculty in # organization and institute in organizational_unit. We can add them # as different fields as long as we make sure the x509 fields are # preserved. # We do that to allow autocreate updating existing cert users. if org_unit not in ('', 'NA'): org_unit = org_unit.upper() oid_extras['faculty'] = org oid_extras['institute'] = org_unit org = org_unit.upper() org_unit = 'NA' # Stay on virtual host - extra useful while we test dual OpenID base_url = environ.get('REQUEST_URI', base_url).split('?')[0].replace('autocreate', 'fileman') raw_login = None for oid_provider in configuration.user_openid_providers: openid_prefix = oid_provider.rstrip('/') + '/' if identity.startswith(openid_prefix): raw_login = identity.replace(openid_prefix, '') break if raw_login: openid_names.append(raw_login) # we should have the proxy file read... proxy_content = accepted['proxy_upload'][-1] # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') user_dict = { 'short_id': uniq_id, 'full_name': full_name, 'organization': org, 'organizational_unit': org_unit, 'locality': locality, 'state': state, 'country': country, 'email': email, 'role': role, 'timezone': timezone, 'password': '', 'comment': '%s: %s' % ('Existing certificate', comment), 'openid_names': openid_names, } user_dict.update(oid_extras) # We must receive some ID from the provider if not uniq_id and not email: output_objects.append( {'object_type': 'error_text', 'text' : 'No ID information received!'}) if accepted.get('openid.sreg.required', '') and \ identity: # Stay on virtual host - extra useful while we test dual OpenID url = environ.get('REQUEST_URI', base_url).split('?')[0].replace('autocreate', 'logout') output_objects.append( {'object_type': 'text', 'text': '''Please note that sign-up for OpenID access does not work if you are already signed in with your OpenID provider - and that appears to be the case now. You probably have to reload this page after you explicitly '''}) output_objects.append( {'object_type': 'link', 'destination': url, 'target': '_blank', 'text': "Logout" }) return (output_objects, returnvalues.CLIENT_ERROR) if login_type == 'cert': user_dict['expire'] = int(time.time() + cert_valid_days * 24 * 60 * 60) try: distinguished_name_to_user(uniq_id) user_dict['distinguished_name'] = uniq_id except: output_objects.append({'object_type': 'error_text', 'text' : '''Illegal Distinguished name: Please note that the distinguished name must be a valid certificate DN with multiple "key=val" fields separated by "/". '''}) return (output_objects, returnvalues.CLIENT_ERROR) elif login_type == 'oid': user_dict['expire'] = int(time.time() + oid_valid_days * 24 * 60 * 60) fill_distinguished_name(user_dict) uniq_id = user_dict['distinguished_name'] # If server allows automatic addition of users with a CA validated cert # we create the user immediately and skip mail if login_type == 'cert' and configuration.auto_add_cert_user or \ login_type == 'oid' and configuration.auto_add_oid_user: fill_user(user_dict) logger.info('create user: %s' % user_dict) # Now all user fields are set and we can begin adding the user db_path = os.path.join(configuration.mig_server_home, user_db_filename) try: create_user(user_dict, configuration.config_file, db_path, ask_renew=False, default_renew=True) if configuration.site_enable_griddk and \ accepted['proxy_upload'] != ['']: # save the file, display expiration date proxy_out = handle_proxy(proxy_content, uniq_id, configuration) output_objects.extend(proxy_out) except Exception, err: logger.error('create failed for %s: %s' % (uniq_id, err)) output_objects.append( {'object_type': 'error_text', 'text' : '''Could not create the user account for you: Please report this problem to the grid administrators (%s).''' % \ admin_email}) return (output_objects, returnvalues.SYSTEM_ERROR) output_objects.append({'object_type': 'html_form', 'text' : '''Created the user account for you - please open <a href="%s">your personal page</a> to proceed using it. ''' % base_url}) return (output_objects, returnvalues.OK)
def send_email(recipients, subject, message, logger, configuration, files=[], custom_sender=None): """Send message to recipients by email: Force utf8 encoding to avoid accented characters appearing garbled. The optional custom_sender can be used to set the email sender in one of two ways depending on the configuration option smtp_send_as_user. In case that option is enabled custom_sender will be used as from and envelope sender address, the latter of which is used e.g. for bounces. It should be noted that doing so comes with the risk of legit mail getting spam-filtered because of failed SPF checks. If the configuration does not set smtp_send_as_user custom_sender will only be used as the Reply-To address which is still convenient when users send out invitations and trigger various requests that may receive manual replies. """ if recipients.find(', ') > -1: recipients_list = recipients.split(', ') else: recipients_list = [recipients] sender_email = from_email = configuration.smtp_sender reply_to_email = configuration.smtp_reply_to if custom_sender: # Only use custom envelope sender if specifically configured! if configuration.smtp_send_as_user: from_email = sender_email = custom_sender reply_to_email = '' else: reply_to_email = custom_sender try: mime_msg = MIMEMultipart() mime_msg['From'] = from_email mime_msg['To'] = recipients if reply_to_email: mime_msg['Reply-To'] = reply_to_email mime_msg['Date'] = formatdate(localtime=True) mime_msg['Subject'] = subject mime_msg.attach(MIMEText(force_utf8(message), "plain", "utf8")) for name in files: part = MIMEBase('application', "octet-stream") part.set_payload(open(name, "rb").read()) Encoders.encode_base64(part) part.add_header( 'Content-Disposition', 'attachment; filename="%s"' % os.path.basename(name)) mime_msg.attach(part) logger.debug('sending email from %s to %s:\n%s' % (from_email, recipients, mime_msg.as_string())) server = smtplib.SMTP(configuration.smtp_server) server.set_debuglevel(0) errors = server.sendmail(sender_email, recipients_list, mime_msg.as_string()) server.quit() if errors: logger.warning('Partial error(s) sending email: %s' % errors) return False else: logger.debug('Email was sent to %s' % recipients) return True except Exception, err: logger.error('Sending email to %s through %s failed!: %s' % (recipients, configuration.smtp_server, str(err))) return False
def validate_authentication(self, username, password, handler): """Password auth against internal DB built from login_map. Please note that we take serious steps to secure against password cracking, but that it _may_ still be possible to achieve with a big effort. The following is checked before granting auth: 1) Valid username 2) Valid user (Does user exist and enabled ftps) 3) Valid 2FA session (if 2FA is enabled) 4) Hit rate limit (Too many auth attempts) 5) Valid password (if password enabled) """ secret = None disconnect = False strict_password_policy = True password_offered = None password_enabled = False invalid_username = False invalid_user = False valid_password = False valid_twofa = False exceeded_rate_limit = False client_ip = handler.remote_ip client_port = handler.remote_port username = force_utf8(username) daemon_conf = configuration.daemon_conf max_user_hits = daemon_conf['auth_limits']['max_user_hits'] user_abuse_hits = daemon_conf['auth_limits']['user_abuse_hits'] proto_abuse_hits = daemon_conf['auth_limits']['proto_abuse_hits'] max_secret_hits = daemon_conf['auth_limits']['max_secret_hits'] logger.debug("Run authentication of %s from %s" % (username, client_ip)) logger.info("refresh user %s" % username) logger.debug("daemon_conf['allow_password']: %s" % daemon_conf['allow_password']) # For e.g. GDP we require all logins to match active 2FA session IP, # but otherwise user may freely switch net during 2FA lifetime. if configuration.site_twofactor_strict_address: enforce_address = client_ip else: enforce_address = None # We don't have a handle_request for server so expire here instead if self.last_expire + self.min_expire_delay < time.time(): self.last_expire = time.time() expire_rate_limit(configuration, "ftps", expire_delay=self.min_expire_delay) if hit_rate_limit(configuration, 'ftps', client_ip, username, max_user_hits=max_user_hits): exceeded_rate_limit = True elif not default_username_validator(configuration, username): invalid_username = True elif daemon_conf['allow_password']: hash_cache = daemon_conf['hash_cache'] password_offered = password secret = make_scramble(password_offered, None) # Only sharelinks should be excluded from strict password policy if configuration.site_enable_sharelinks and \ possible_sharelink_id(configuration, username): strict_password_policy = False logger.debug("refresh user %s" % username) self._update_logins(configuration, username) if not self.has_user(username): if not os.path.islink( os.path.join(daemon_conf['root_dir'], username)): invalid_user = True entries = [] else: # list of User login objects for username entries = [self.user_table[username]] for entry in entries: if entry['pwd'] is not None: password_enabled = True password_allowed = entry['pwd'] logger.debug("Password check for %s" % username) if check_password_hash(configuration, 'ftps', username, password_offered, password_allowed, hash_cache, strict_password_policy): valid_password = True break if valid_password and check_twofactor_session( configuration, username, enforce_address, 'ftps'): valid_twofa = True # Update rate limits and write to auth log (authorized, disconnect) = validate_auth_attempt( configuration, 'ftps', 'password', username, client_ip, client_port, secret=secret, invalid_username=invalid_username, invalid_user=invalid_user, valid_twofa=valid_twofa, authtype_enabled=password_enabled, valid_auth=valid_password, exceeded_rate_limit=exceeded_rate_limit, user_abuse_hits=user_abuse_hits, proto_abuse_hits=proto_abuse_hits, max_secret_hits=max_secret_hits, ) if disconnect: handler._shutdown_connecting_dtp() if authorized: self.authenticated_user = username return True else: # Must raise AuthenticationFailed exception since version 1.0.0 instead # of returning bool self.authenticated_user = None raise AuthenticationFailed()
def main(client_id, user_arguments_dict): """Main function used by front end""" (configuration, logger, output_objects, op_name) = \ initialize_main_variables(client_id, op_header=False, op_menu=False) defaults = signature()[1] (validate_status, accepted) = validate_input(user_arguments_dict, defaults, output_objects, allow_rejects=False) if not validate_status: logger.warning('%s invalid input: %s' % (op_name, accepted)) return (accepted, returnvalues.CLIENT_ERROR) if not configuration.site_enable_openid or \ not 'migoid' in configuration.site_signup_methods: output_objects.append( {'object_type': 'error_text', 'text': '''Local OpenID login is not enabled on this site'''}) return (output_objects, returnvalues.SYSTEM_ERROR) title_entry = find_entry(output_objects, 'title') title_entry['text'] = '%s OpenID account request' % \ configuration.short_title title_entry['skipmenu'] = True output_objects.append({'object_type': 'header', 'text': '%s OpenID account request' % configuration.short_title }) admin_email = configuration.admin_email smtp_server = configuration.smtp_server user_pending = os.path.abspath(configuration.user_pending) # force name to capitalized form (henrik karlsen -> Henrik Karlsen) # please note that we get utf8 coded bytes here and title() treats such # chars as word termination. Temporarily force to unicode. raw_name = accepted['cert_name'][-1].strip() try: cert_name = force_utf8(force_unicode(raw_name).title()) except Exception: cert_name = raw_name.title() country = accepted['country'][-1].strip().upper() state = accepted['state'][-1].strip().title() org = accepted['org'][-1].strip() # lower case email address email = accepted['email'][-1].strip().lower() password = accepted['password'][-1] verifypassword = accepted['verifypassword'][-1] # The checkbox typically returns value 'on' if selected passwordrecovery = (accepted['passwordrecovery'][-1].strip().lower() in ('1', 'o', 'y', 't', 'on', 'yes', 'true')) # keep comment to a single line comment = accepted['comment'][-1].replace('\n', ' ') # single quotes break command line format - remove comment = comment.replace("'", ' ') if not safe_handler(configuration, 'post', op_name, client_id, get_csrf_limit(configuration), accepted): output_objects.append( {'object_type': 'error_text', 'text': '''Only accepting CSRF-filtered POST requests to prevent unintended updates''' }) return (output_objects, returnvalues.CLIENT_ERROR) if password != verifypassword: output_objects.append({'object_type': 'error_text', 'text': 'Password and verify password are not identical!' }) output_objects.append( {'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again"}) return (output_objects, returnvalues.CLIENT_ERROR) try: assure_password_strength(configuration, password) except Exception, exc: logger.warning( "%s invalid password for '%s' (policy %s): %s" % (op_name, cert_name, configuration.site_password_policy, exc)) output_objects.append({'object_type': 'error_text', 'text': 'Invalid password requested: %s.' % exc }) output_objects.append( {'object_type': 'link', 'destination': 'javascript:history.back();', 'class': 'genericbutton', 'text': "Try again"}) return (output_objects, returnvalues.CLIENT_ERROR)