Ejemplo n.º 1
0
def create_app(env=os.environ):
    """Create an instance of the web application"""
    # setup our app config
    app = Flask(__name__)
    app.secret_key = '\x08~m\xde\x87\xda\x17\x7f!\x97\xdf_@%\xf1{\xaa\xd8)\xcbU\xfe\x94\xc4'
    app.jinja_env.globals['csrf_token'] = generate_csrf_token

    if env.get('ENV') == 'production':
        csp = {
            'default-src':
            '\'self\'',
            'img-src':
            '*',
            'style-src': [
                '\'self\'', '*.cloud.gov', '*.googleapis.com',
                '\'unsafe-inline\''
            ],
            'font-src': ['\'self\'', 'fonts.gstatic.com', '*.cloud.gov']
        }
        Talisman(app, content_security_policy=csp)

    @app.after_request
    def set_headers(response):
        response.cache_control.no_cache = True
        response.cache_control.no_store = True
        response.cache_control.must_revalidate = True
        response.cache_control.private = True
        response.headers['Pragma'] = 'no-cache'
        return response

    # copy these environment variables into app.config
    for ck, default in CONFIG_KEYS.items():
        app.config[ck] = env.get(ck, default)

    # do boolean checks on this variable
    app.config['UAA_VERIFY_TLS'] = str_to_bool(app.config['UAA_VERIFY_TLS'])

    # make sure our base url doesn't have a trailing slash as UAA will flip out
    app.config['UAA_BASE_URL'] = app.config['UAA_BASE_URL'].rstrip('/')

    logging.info('Loaded application configuration:')
    for ck in sorted(CONFIG_KEYS.keys()):
        logging.info('{0}: {1}'.format(ck, app.config[ck]))

    @app.before_request
    def have_uaa_and_csrf_token():
        """Before each request, make sure we have a valid token from UAA.

        If we don't send them to UAA to start the oauth process.

        Technically we should bounce them through the renew token process if we already have one,
        but this app will be used sparingly, so it's fine to push them back through the authorize flow
        each time we need to renew our token.

        """
        # don't authenticate the oauth code receiver, or we'll never get the code back from UAA
        if request.endpoint and request.endpoint in [
                'oauth_login', 'forgot_password', 'reset_password', 'static'
        ]:
            return

        # check our token, and expirary date
        token = session.get('UAA_TOKEN', None)

        # if all looks good, setup the client
        if token:
            g.uaac = UAAClient(app.config['UAA_BASE_URL'],
                               session['UAA_TOKEN'],
                               verify_tls=app.config['UAA_VERIFY_TLS'])
        else:
            # if not forget the token, it's bad (if we have one)
            session.clear()
            session['_endpoint'] = request.endpoint

            return redirect(
                '{0}/oauth/authorize?client_id={1}&response_type=code'.format(
                    app.config['UAA_BASE_URL'], app.config['UAA_CLIENT_ID']))

        # if it's a POST request, that's not to oauth_login
        # Then check for a CSRF token, if we don't have one, bail
        if request.method == "POST":
            csrf_token = session.pop('_csrf_token', None)
            if not csrf_token or csrf_token != request.form.get('_csrf_token'):
                logging.error(
                    'Error validating CSRF token.  Got: {0}; Expected: {1}'.
                    format(request.form.get('_csrf_token'), csrf_token))

                return render_template('error/csrf.html'), 400

    @app.route('/oauth/login')
    def oauth_login():
        """Called at the end of the oauth flow.  We'll receive an auth code from UAA and use it to
        retrieve a bearer token that we can use to actually do stuff
        """
        logging.info(request.referrer)
        is_invitation = request.referrer and request.referrer.find(
            'invitations/accept') != 1
        if is_invitation and 'code' not in request.args:
            return redirect(url_for('first_login'))

        try:

            # connect a client with no token
            uaac = UAAClient(app.config['UAA_BASE_URL'],
                             None,
                             verify_tls=app.config['UAA_VERIFY_TLS'])

            # auth with our client secret and the code they gave us
            token = uaac.oauth_token(request.args['code'],
                                     app.config['UAA_CLIENT_ID'],
                                     app.config['UAA_CLIENT_SECRET'])

            # if it's valid, but missing the scope we need, bail
            if 'scim.invite' not in token['scope'].split(' '):
                raise RuntimeError(
                    'Valid oauth authentication but missing the scim.invite scope.  Scopes: {0}'
                    .format(token['scope']))

            # make flask expire our session for us, by expiring it shortly before the token expires
            session.permanent = True
            app.permanent_session_lifetime = timedelta(
                seconds=token['expires_in'] - 30)

            # stash the stuff we care about
            session['UAA_TOKEN'] = token['access_token']
            session['UAA_TOKEN_SCOPES'] = token['scope'].split(' ')
            if is_invitation:
                return redirect(url_for('first_login'))
            endpoint = session.pop('_endpoint', None)
            if not endpoint:
                endpoint = 'index'
            logging.info(endpoint)
            return redirect(url_for(endpoint))
        except UAAError:
            logging.exception(
                'An invalid authorization_code was received from UAA')
            return render_template('error/token_validation.html'), 401
        except RuntimeError:
            logging.exception('Token validated but had wrong scope')
            return render_template('error/missing_scope.html'), 403

    @app.route('/', methods=['GET', 'POST'])
    def index():
        return render_template('index.html')

    @app.route('/invite', methods=['GET', 'POST'])
    def invite():
        # start with giving them the form
        if request.method == 'GET':
            return render_template('invite.html')

        # if we've reached here we are POST, and they've asked us to invite

        # validate the email address
        email = request.form.get('email', '')
        if not email:
            flash('Email cannot be blank.')
            return render_template('invite.html')
        try:
            v = validate_email(email)  # validate and get info
            email = v["email"]  # replace with normalized form
        except EmailNotValidError as exc:
            # email is not valid, exception message is human-readable
            flash(str(exc))
            return render_template('invite.html')

        # email is good, lets invite them
        try:
            if g.uaac.does_origin_user_exist(
                    app.config['UAA_CLIENT_ID'],
                    app.config['UAA_CLIENT_SECRET'], email,
                    app.config['IDP_PROVIDER_ORIGIN']):
                flash('User already has a valid account.')
                return render_template('invite.html')

            redirect_uri = os.path.join(request.url_root, 'oauth', 'login')
            logging.info('redirect for invite: {0}'.format(redirect_uri))
            invite = g.uaac.invite_users(email, redirect_uri)

            if len(invite['failed_invites']):
                raise RuntimeError('UAA failed to invite the user.')

            invite = invite['new_invites'][0]

            branding = {'company_name': app.config['BRANDING_COMPANY_NAME']}

            # we invited them, send them the link to validate their account
            subject = render_template('email/subject.txt',
                                      invite=invite,
                                      branding=branding).strip()
            body = render_template('email/body.html',
                                   invite=invite,
                                   branding=branding)

            send_email(app, email, subject, body)
            return render_template('invite_sent.html')
        except UAAError as exc:
            # if UAA complains that our access token is invalid then force them back through the login
            # process.
            # TODO: Fix this properly by implementing the refresh token oauth flow
            # in the UAAClient when it detects it's token is no longer valid
            if 'Invalid access token' in str(exc):
                return redirect(url_for('logout'))
            else:
                logging.exception('An error occured communicating with UAA')
        except Exception:
            logging.exception('An error occured during the invite process')

        return render_template('error/internal.html'), 500

    @app.route('/first-login', methods=['GET'])
    def first_login():

        # check our token, and expirary date
        token = session.get('UAA_TOKEN', None)

        try:
            decoded_token = g.uaac.decode_access_token(token)
        except:
            logging.exception('An invalid access token was decoded')
            return render_template('error/token_validation.html'), 401

        user = g.uaac.get_user(decoded_token['user_id'])
        logging.info('USER: {0}'.format(user))
        if user['origin'] == 'uaa':
            user['origin'] = app.config['IDP_PROVIDER_ORIGIN']
            user['externalId'] = user['userName']
            g.uaac.put_user(user)
        if user['origin'] == app.config['IDP_PROVIDER_ORIGIN']:
            return redirect(app.config['IDP_PROVIDER_URL'])
        else:
            return redirect(app.config['UAA_BASE_URL'])

    @app.route('/change-password', methods=['GET', 'POST'])
    def change_password():
        # start with giving them the form
        if request.method == 'GET':
            return render_template('change_password.html')

        # if we've reached here we are POST so change the pass

        # validate new password
        old_password = request.form.get('old_password', '')
        new_password = request.form.get('new_password', '')
        repeat_password = request.form.get('repeat_password', '')
        errors = []
        if old_password == new_password or old_password == repeat_password:
            errors.append('Your new password cannot match your old password.')
        if not old_password:
            errors.append('Your old password cannot be blank.')
        if not new_password:
            errors.append('Your new password cannot be blank.')
        if not repeat_password:
            errors.append('You must repeat your new password.')
        if new_password != repeat_password:
            errors.append(
                'Your new password does not match your repeated password.')

        if len(errors) != 0:
            for error in errors:
                flash(error)
            return render_template('change_password.html')

        # check our token, and expirary date
        token = session.get('UAA_TOKEN', None)

        try:
            decoded_token = g.uaac.decode_access_token(token)
        except:
            logging.exception('An invalid access token was decoded')
            return render_template('error/token_validation.html'), 401

        try:
            g.uaac.change_password(decoded_token['user_id'], old_password,
                                   new_password)
            return render_template('password_changed.html')
        except UAAError as exc:
            for error in str(exc).split(','):
                flash(error)
            return render_template('change_password.html')
        except Exception:
            logging.exception('Error changing password')

        return render_template('error/internal.html'), 500

    @app.route('/forgot-password', methods=['GET', 'POST'])
    def forgot_password():
        identity_token = uuid.uuid4().hex

        # start with giving them the form
        if request.method == 'GET':
            return render_template('forgot_password.html')

        # if we've reached here we are POST so we can email user link
        email = request.form.get('email_address', '')
        if not email:
            flash('Email cannot be blank.')
            return render_template('forgot_password.html')
        try:
            v = validate_email(email)  # validate and get info
            email = v["email"]  # replace with normalized form
        except EmailNotValidError as exc:
            # email is not valid, exception message is human-readable
            flash(str(exc))
            return render_template('forgot_password.html')

        if r:
            # If we've made it this far, it's a valid email so we'll generate and store a
            # token and send an email.
            logging.info('generating validation token for user')

            branding = {'company_name': app.config['BRANDING_COMPANY_NAME']}

            reset = {
                'verifyLink':
                url_for('reset_password',
                        validation=identity_token,
                        _external=True)
            }
            logging.info(reset['verifyLink'])

            subject = render_template('email/subject-password.txt',
                                      reset=reset,
                                      branding=branding).strip()
            body = render_template('email/body-password.html',
                                   reset=reset,
                                   branding=branding)
            send_email(app, email, subject, body)
            r.setex(email, FORGOT_PW_TOKEN_EXPIRATION_IN_SECONDS,
                    identity_token)

            return render_template('forgot_password.html',
                                   email_sent=True,
                                   email=email)

        return render_template('error/internal.html'), 500

    @app.route('/reset-password', methods=['GET', 'POST'])
    def reset_password():

        # start with giving them the form
        if request.method == 'GET':

            if 'validation' not in request.args:
                flash(
                    'The password validation link is incomplete. Please verify your link is correct and try again.'
                )
                return render_template('reset_password.html',
                                       validation_code=None)

            return render_template('reset_password.html',
                                   validation_code=request.args['validation'])

        # if we've reached here we are POST so we can email user link
        token = request.form.get('_validation_code', '')
        email = request.form.get('email_address', '')
        if not email:
            flash('Email cannot be blank.')
            return render_template('reset_password.html')
        try:
            v = validate_email(email)  # validate and get info
            email = v["email"]  # replace with normalized form
        except EmailNotValidError as exc:
            # email is not valid, exception message is human-readable
            flash(str(exc))
            return render_template('reset_password.html')

        # If we've made it this far, it's a valid email so let's verify the generated
        # token with their email address.
        if r:
            userToken = r.get(email)

            if userToken.decode('utf-8') == token:
                logging.info('Successfully verified token {0} for {1}'.format(
                    userToken, email))
                r.delete(email)
            else:
                flash(
                    'Valid token not found. Please try your forgot password request again.'
                )
                return render_template('reset_password.html')

            temporaryPassword = generate_temporary_password()
            try:
                g.uaac = UAAClient(app.config['UAA_BASE_URL'],
                                   None,
                                   verify_tls=app.config['UAA_VERIFY_TLS'])
                if g.uaac.set_temporary_password(
                        app.config['UAA_CLIENT_ID'],
                        app.config['UAA_CLIENT_SECRET'], email,
                        temporaryPassword):
                    logging.info(
                        'Set temporary password for {0}'.format(email))
                    return render_template('reset_password.html',
                                           password=temporaryPassword)
                else:
                    flash(
                        'Unable to set temporary password. Did you use the right email address?'
                    )
                    return render_template('reset_password.html')
            except Exception:
                logging.exception('Unable to set your temporary password.')

        return render_template('error/internal.html'), 500

    @app.route('/logout')
    def logout():
        session.clear()

        return redirect(url_for('index'))

    threadEvent = threading.Event()

    app.config['SERVER_NAME'] = SERVER_NAME
    app.config['PREFERRED_URL_SCHEME'] = PREFERRED_URL_SCHEME
    with app.app_context():
        changeLink = url_for('change_password', _external=True)

        def expiration_job():
            do_expiring_pw_notifications(app, changeLink)

        scheduler = Scheduler()

        minutes = random.randrange(10)
        tens_of_minutes = random.randrange(3)
        # somewhere between 08:00 and 08:29 based on randomness above
        job_time = "08:{0}{1}".format(tens_of_minutes, minutes)
        scheduler.every().day.at(job_time).do(expiration_job)

        schedThread = scheduler.daemon_thread(event=threadEvent)
        schedThread.start()

    def cease_scheduler(signum, _frame):
        threadEvent.set()
        schedThread.join(1)

    signal.signal(signal.SIGTERM, cease_scheduler)
    signal.signal(signal.SIGALRM, cease_scheduler)

    return app