Beispiel #1
0
    def generate_access_token(self, _id):
        """
        Manually generate an OAuth access token for the given consumer.

        NB: Manually generated access tokens are bearer tokens, which are
        less secure (since they rely only on the token, which is transmitted
        with each request, unlike the access token secret).
        """
        consumer_token = M.OAuthConsumerToken.query.get(_id=bson.ObjectId(_id))
        if consumer_token is None:
            flash('Invalid app ID', 'error')
            redirect('.')
        if consumer_token.user_id != c.user._id:
            flash('Invalid app ID', 'error')
            redirect('.')
        request_token = M.OAuthRequestToken(
            consumer_token_id=consumer_token._id,
            user_id=c.user._id,
            callback='manual',
            validation_pin=h.nonce(20),
            is_bearer=True,
        )
        access_token = M.OAuthAccessToken(
            consumer_token_id=consumer_token._id,
            request_token_id=c.user._id,
            user_id=request_token.user_id,
            is_bearer=True,
        )
        redirect('.')
Beispiel #2
0
    def generate_access_token(self, _id):
        """
        Manually generate an OAuth access token for the given consumer.

        NB: Manually generated access tokens are bearer tokens, which are
        less secure (since they rely only on the token, which is transmitted
        with each request, unlike the access token secret).
        """
        consumer_token = M.OAuthConsumerToken.query.get(_id=bson.ObjectId(_id))
        if consumer_token is None:
            flash('Invalid app ID', 'error')
            redirect('.')
        if consumer_token.user_id != c.user._id:
            flash('Invalid app ID', 'error')
            redirect('.')
        request_token = M.OAuthRequestToken(
            consumer_token_id=consumer_token._id,
            user_id=c.user._id,
            callback='manual',
            validation_pin=h.nonce(20),
            is_bearer=True,
        )
        M.OAuthAccessToken(
            consumer_token_id=consumer_token._id,
            request_token_id=c.user._id,
            user_id=request_token.user_id,
            is_bearer=True,
        )
        redirect('.')
Beispiel #3
0
class ApiTicket(MappedClass, ApiAuthMixIn):
    class __mongometa__:
        name = 'api_ticket'
        session = main_orm_session

    PREFIX = 'tck'

    _id = FieldProperty(S.ObjectId)
    user_id = ForeignIdProperty('User')
    api_key = FieldProperty(str,
                            if_missing=lambda: ApiTicket.PREFIX + h.nonce(20))
    secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)
    expires = FieldProperty(datetime, if_missing=None)
    capabilities = FieldProperty({str: None})
    mod_date = FieldProperty(datetime, if_missing=datetime.utcnow)

    user = RelationProperty('User')

    @classmethod
    def get(cls, api_ticket):
        if not api_ticket.startswith(cls.PREFIX):
            return None
        return cls.query.get(api_key=api_ticket)

    def authenticate_request(self, path, params):
        if self.expires and datetime.utcnow() > self.expires:
            return False
        return ApiAuthMixIn.authenticate_request(self, path, params)

    def get_capability(self, key):
        return self.capabilities.get(key)
Beispiel #4
0
    def password_recovery_hash(self, email=None, **kw):
        provider = plugin.AuthenticationProvider.get(request)
        if not provider.forgotten_password_process:
            raise wexc.HTTPNotFound()
        if not email:
            redirect('/')
        user_record = M.User.by_email_address(email)
        hash = h.nonce(42)
        user_record.set_tool_data('AuthPasswordReset',
                                  hash=hash,
                                  hash_expiry=datetime.datetime.utcnow() +
                                  datetime.timedelta(seconds=int(config.get('auth.recovery_hash_expiry_period', 600))))

        log.info('Sending password recovery link to %s', email)
        text = '''
To reset your password on %s, please visit the following URL:

%s/auth/forgotten_password/%s

''' % (config['site_name'], config['base_url'], hash)

        allura.tasks.mail_tasks.sendmail.post(
            destinations=[email],
            fromaddr=config['forgemail.return_path'],
            reply_to=config['forgemail.return_path'],
            subject='Password recovery',
            message_id=h.gen_message_id(),
            text=text)

        flash('Email with instructions has been sent.')
        redirect('/')
 def _update_mounts(self, subproject=None, tool=None, new=None, **kw):
     if subproject is None:
         subproject = []
     if tool is None:
         tool = []
     for sp in subproject:
         p = M.Project.query.get(shortname=sp['shortname'],
                                 neighborhood_id=c.project.neighborhood_id)
         if sp.get('delete'):
             require_access(c.project, 'admin')
             M.AuditLog.log('delete subproject %s', sp['shortname'])
             h.log_action(log, 'delete subproject').info(
                 'delete subproject %s', sp['shortname'],
                 meta=dict(name=sp['shortname']))
             p.removal = 'deleted'
             plugin.ProjectRegistrationProvider.get().delete_project(
                 p, c.user)
         elif not new:
             M.AuditLog.log('update subproject %s', sp['shortname'])
             p.name = sp['name']
             p.ordinal = int(sp['ordinal'])
     for p in tool:
         if p.get('delete'):
             require_access(c.project, 'admin')
             M.AuditLog.log('uninstall tool %s', p['mount_point'])
             h.log_action(log, 'uninstall tool').info(
                 'uninstall tool %s', p['mount_point'],
                 meta=dict(mount_point=p['mount_point']))
             c.project.uninstall_app(p['mount_point'])
         elif not new:
             M.AuditLog.log('update tool %s', p['mount_point'])
             options = c.project.app_config(p['mount_point']).options
             options.mount_label = p['mount_label']
             options.ordinal = int(p['ordinal'])
     if new and new.get('install'):
         ep_name = new.get('ep_name', None)
         if not ep_name:
             require_access(c.project, 'create')
             mount_point = new['mount_point'].lower() or h.nonce()
             M.AuditLog.log('create subproject %s', mount_point)
             h.log_action(log, 'create subproject').info(
                 'create subproject %s', mount_point,
                 meta=dict(mount_point=mount_point, name=new['mount_label']))
             sp = c.project.new_subproject(mount_point)
             sp.name = new['mount_label']
             sp.ordinal = int(new['ordinal'])
         else:
             require_access(c.project, 'admin')
             installable_tools = AdminApp.installable_tools_for(c.project)
             if not ep_name.lower() in [t['name'].lower() for t in installable_tools]:
                 flash('Installation limit exceeded.', 'error')
                 return
             mount_point = new['mount_point'] or ep_name
             M.AuditLog.log('install tool %s', mount_point)
             h.log_action(log, 'install tool').info(
                 'install tool %s', mount_point,
                 meta=dict(tool_type=ep_name, mount_point=mount_point, mount_label=new['mount_label']))
             c.project.install_app(
                 ep_name, mount_point, mount_label=new['mount_label'], ordinal=new['ordinal'])
     g.post_event('project_updated')
 def update_configuration(self, divs=None, layout_class=None, new_div=None, **kw):
     require_access(c.project, 'update')
     config = M.PortalConfig.current()
     config.layout_class = layout_class
     # Handle updated and deleted divs
     if divs is None: divs = []
     new_divs = []
     for div in divs:
         log.info('Got div update:%s', pformat(div))
         if div.get('del'): continue
         new_divs.append(div)
     # Handle new divs
     if new_div:
         new_divs.append(dict(name=h.nonce(), content=[]))
     config.layout = []
     for div in new_divs:
         content = []
         for w in div.get('content', []):
             if w.get('del'): continue
             mp,wn = w['widget'].split('/')
             content.append(dict(mount_point=mp, widget_name=wn))
         if div.get('new_widget'):
             content.append(dict(mount_point='profile', widget_name='welcome'))
         config.layout.append(dict(
                 name=div['name'],
                 content=content))
     redirect('configuration')
Beispiel #7
0
 def test_bearer_token_valid(self, request):
     user = M.User.by_username('test-admin')
     consumer_token = M.OAuthConsumerToken(
         name='foo',
         description='foo app',
     )
     request_token = M.OAuthRequestToken(
         consumer_token_id=consumer_token._id,
         user_id=user._id,
         callback='manual',
         validation_pin=h.nonce(20),
         is_bearer=True,
     )
     access_token = M.OAuthAccessToken(
         consumer_token_id=consumer_token._id,
         request_token_id=request_token._id,
         user_id=user._id,
         is_bearer=True,
     )
     ThreadLocalODMSession.flush_all()
     request.headers = {}
     request.params = {'access_token': access_token.api_key}
     request.scheme = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo')
     assert_equal(r.status_int, 200)
Beispiel #8
0
 def test_bearer_token_valid_via_headers(self, request):
     user = M.User.by_username('test-admin')
     consumer_token = M.OAuthConsumerToken(
         name='foo',
         description='foo app',
     )
     request_token = M.OAuthRequestToken(
         consumer_token_id=consumer_token._id,
         user_id=user._id,
         callback='manual',
         validation_pin=h.nonce(20),
         is_bearer=True,
     )
     access_token = M.OAuthAccessToken(
         consumer_token_id=consumer_token._id,
         request_token_id=request_token._id,
         user_id=user._id,
         is_bearer=True,
     )
     ThreadLocalODMSession.flush_all()
     token = access_token.api_key
     request.headers = {
         'Authorization': 'Bearer {}'.format(token)
     }
     request.scheme = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo', status=200)
     # reverse proxy situation
     request.scheme = 'http'
     request.environ['paste.testing'] = False
     request.environ['HTTP_X_FORWARDED_PROTOx'] = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo', status=200)
 def update_configuration(self, divs=None, layout_class=None, new_div=None, **kw):
     require_access(c.project, 'update')
     config = M.PortalConfig.current()
     config.layout_class = layout_class
     # Handle updated and deleted divs
     if divs is None: divs = []
     new_divs = []
     for div in divs:
         log.info('Got div update:%s', pformat(div))
         if div.get('del'): continue
         new_divs.append(div)
     # Handle new divs
     if new_div:
         new_divs.append(dict(name=h.nonce(), content=[]))
     config.layout = []
     for div in new_divs:
         content = []
         for w in div.get('content', []):
             if w.get('del'): continue
             mp,wn = w['widget'].split('/')
             content.append(dict(mount_point=mp, widget_name=wn))
         if div.get('new_widget'):
             content.append(dict(mount_point='profile', widget_name='welcome'))
         config.layout.append(dict(
                 name=div['name'],
                 content=content))
     redirect('configuration')
Beispiel #10
0
 def test_bearer_token_valid_via_headers(self, request):
     user = M.User.by_username('test-admin')
     consumer_token = M.OAuthConsumerToken(
         name='foo',
         description='foo app',
     )
     request_token = M.OAuthRequestToken(
         consumer_token_id=consumer_token._id,
         user_id=user._id,
         callback='manual',
         validation_pin=h.nonce(20),
         is_bearer=True,
     )
     access_token = M.OAuthAccessToken(
         consumer_token_id=consumer_token._id,
         request_token_id=request_token._id,
         user_id=user._id,
         is_bearer=True,
     )
     ThreadLocalODMSession.flush_all()
     token = access_token.api_key
     request.headers = {'Authorization': 'Bearer {}'.format(token)}
     request.scheme = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo', status=200)
     # reverse proxy situation
     request.scheme = 'http'
     request.environ['paste.testing'] = False
     request.environ['HTTP_X_FORWARDED_PROTOx'] = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo', status=200)
Beispiel #11
0
 def test_bearer_token_valid(self, request):
     user = M.User.by_username('test-admin')
     consumer_token = M.OAuthConsumerToken(
         name='foo',
         description='foo app',
     )
     request_token = M.OAuthRequestToken(
         consumer_token_id=consumer_token._id,
         user_id=user._id,
         callback='manual',
         validation_pin=h.nonce(20),
         is_bearer=True,
     )
     access_token = M.OAuthAccessToken(
         consumer_token_id=consumer_token._id,
         request_token_id=request_token._id,
         user_id=user._id,
         is_bearer=True,
     )
     ThreadLocalODMSession.flush_all()
     request.headers = {}
     request.params = {'access_token': access_token.api_key}
     request.scheme = 'https'
     r = self.api_post('/rest/p/test/wiki', access_token='foo')
     assert_equal(r.status_int, 200)
Beispiel #12
0
    def token(self, username):
        if self._use_token:
            return self._use_token

        # only create token once, else ming gets dupe key error
        if username not in self._token_cache:
            user = M.User.query.get(username=username)
            consumer_token = M.OAuthConsumerToken(
                name='test-%s' % str(user._id),
                description='test-app-%s' % str(user._id),
                user_id=user._id)
            request_token = M.OAuthRequestToken(
                consumer_token_id=consumer_token._id,
                user_id=user._id,
                callback='manual',
                validation_pin=h.nonce(20))
            token = M.OAuthAccessToken(consumer_token_id=consumer_token._id,
                                       request_token_id=request_token._id,
                                       user_id=user._id,
                                       is_bearer=True)
            ming.orm.session(consumer_token).flush()
            ming.orm.session(request_token).flush()
            ming.orm.session(token).flush()
            self._token_cache[username] = token

        return self._token_cache[username]
Beispiel #13
0
    def token(self, username):
        if self._use_token:
            return self._use_token

        # only create token once, else ming gets dupe key error
        if username not in self._token_cache:
            user = M.User.query.get(username=username)
            consumer_token = M.OAuthConsumerToken(
                name='test-%s' % str(user._id),
                description='test-app-%s' % str(user._id),
                user_id=user._id)
            request_token = M.OAuthRequestToken(
                consumer_token_id=consumer_token._id,
                user_id=user._id,
                callback='manual',
                validation_pin=h.nonce(20))
            token = M.OAuthAccessToken(
                consumer_token_id=consumer_token._id,
                request_token_id=request_token._id,
                user_id=user._id,
                is_bearer=True)
            ming.orm.session(consumer_token).flush()
            ming.orm.session(request_token).flush()
            ming.orm.session(token).flush()
            self._token_cache[username] = token

        return self._token_cache[username]
Beispiel #14
0
 def do_authorize(self, yes=None, no=None, oauth_token=None):
     security.require_authenticated()
     rtok = M.OAuthRequestToken.query.get(api_key=oauth_token)
     if no:
         rtok.delete()
         flash("%s NOT AUTHORIZED" % rtok.consumer_token.name, "error")
         redirect("/auth/oauth/")
     if rtok.callback == "oob":
         rtok.validation_pin = h.nonce(6)
         return dict(rtok=rtok)
     rtok.validation_pin = h.nonce(20)
     if "?" in rtok.callback:
         url = rtok.callback + "&"
     else:
         url = rtok.callback + "?"
     url += "oauth_token=%s&oauth_verifier=%s" % (rtok.api_key, rtok.validation_pin)
     redirect(url)
Beispiel #15
0
 def make_password_reset_url(self):
     hash = h.nonce(42)
     self.set_tool_data('AuthPasswordReset',
                        hash=hash,
                        hash_expiry=datetime.utcnow() +
                                    timedelta(seconds=int(config.get('auth.recovery_hash_expiry_period', 600))))
     reset_url = h.absurl('/auth/forgotten_password/{}'.format(hash))
     return reset_url
Beispiel #16
0
    def password_recovery_hash(self, email=None, **kw):
        provider = plugin.AuthenticationProvider.get(request)
        if not provider.forgotten_password_process:
            raise wexc.HTTPNotFound()
        if not email:
            redirect('/')

        user_record = M.User.by_email_address(email)
        allow_non_primary_email_reset = asbool(
            config.get('auth.allow_non_primary_email_password_reset', True))

        if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
            flash('Enter email in correct format!', 'error')
            redirect('/auth/forgotten_password')

        if not allow_non_primary_email_reset:
            message = 'If the given email address is on record, '\
                      'a password reset email has been sent to the account\'s primary email address.'
            email_record = M.EmailAddress.get(
                email=provider.get_primary_email_address(
                    user_record=user_record),
                confirmed=True)
        else:
            message = 'A password reset email has been sent, if the given email address is on record in our system.'
            email_record = M.EmailAddress.get(email=email, confirmed=True)

        if user_record and email_record and email_record.confirmed:
            hash = h.nonce(42)
            user_record.set_tool_data(
                'AuthPasswordReset',
                hash=hash,
                hash_expiry=datetime.datetime.utcnow() +
                datetime.timedelta(seconds=int(
                    config.get('auth.recovery_hash_expiry_period', 600))))

            log.info('Sending password recovery link to %s',
                     email_record.email)
            subject = '%s Password recovery' % config['site_name']
            text = g.jinja2_env.get_template(
                'allura:templates/mail/forgot_password.txt').render(
                    dict(
                        user=user_record,
                        config=config,
                        hash=hash,
                    ))

            allura.tasks.mail_tasks.sendsimplemail.post(
                toaddr=email_record.email,
                fromaddr=config['forgemail.return_path'],
                reply_to=config['forgemail.return_path'],
                subject=subject,
                message_id=h.gen_message_id(),
                text=text)
        h.auditlog_user('Password recovery link sent to: %s',
                        email,
                        user=user_record)
        flash(message)
        redirect('/')
 def update_mounts(self, subproject=None, tool=None, new=None, **kw):
     if subproject is None:
         subproject = []
     if tool is None:
         tool = []
     for sp in subproject:
         p = M.Project.query.get(shortname=sp["shortname"], neighborhood_id=c.project.neighborhood_id)
         if sp.get("delete"):
             require_access(c.project, "admin")
             M.AuditLog.log("delete subproject %s", sp["shortname"])
             h.log_action(log, "delete subproject").info(
                 "delete subproject %s", sp["shortname"], meta=dict(name=sp["shortname"])
             )
             p.removal = "deleted"
             plugin.ProjectRegistrationProvider.get().delete_project(p, c.user)
         elif not new:
             M.AuditLog.log("update subproject %s", sp["shortname"])
             p.name = sp["name"]
             p.ordinal = int(sp["ordinal"])
     for p in tool:
         if p.get("delete"):
             require_access(c.project, "admin")
             M.AuditLog.log("uninstall tool %s", p["mount_point"])
             h.log_action(log, "uninstall tool").info(
                 "uninstall tool %s", p["mount_point"], meta=dict(mount_point=p["mount_point"])
             )
             c.project.uninstall_app(p["mount_point"])
         elif not new:
             M.AuditLog.log("update tool %s", p["mount_point"])
             options = c.project.app_config(p["mount_point"]).options
             options.mount_label = p["mount_label"]
             options.ordinal = int(p["ordinal"])
     try:
         if new and new.get("install"):
             ep_name = new.get("ep_name", None)
             if not ep_name:
                 require_access(c.project, "create")
                 mount_point = new["mount_point"].lower() or h.nonce()
                 M.AuditLog.log("create subproject %s", mount_point)
                 h.log_action(log, "create subproject").info(
                     "create subproject %s", mount_point, meta=dict(mount_point=mount_point, name=new["mount_label"])
                 )
                 sp = c.project.new_subproject(mount_point)
                 sp.name = new["mount_label"]
                 sp.ordinal = int(new["ordinal"])
             else:
                 require_access(c.project, "admin")
                 mount_point = new["mount_point"].lower() or ep_name.lower()
                 M.AuditLog.log("install tool %s", mount_point)
                 h.log_action(log, "install tool").info(
                     "install tool %s",
                     mount_point,
                     meta=dict(tool_type=ep_name, mount_point=mount_point, mount_label=new["mount_label"]),
                 )
                 c.project.install_app(ep_name, mount_point, mount_label=new["mount_label"], ordinal=new["ordinal"])
     except forge_exc.ForgeError, exc:
         flash("%s: %s" % (exc.__class__.__name__, exc.args[0]), "error")
Beispiel #18
0
 def do_authorize(self, yes=None, no=None, oauth_token=None):
     security.require_authenticated()
     rtok = M.OAuthRequestToken.query.get(api_key=oauth_token)
     if no:
         rtok.delete()
         flash('%s NOT AUTHORIZED' % rtok.consumer_token.name, 'error')
         redirect('/auth/oauth/')
     if rtok.callback == 'oob':
         rtok.validation_pin = h.nonce(6)
         return dict(rtok=rtok)
     rtok.validation_pin = h.nonce(20)
     if '?' in rtok.callback:
         url = rtok.callback + '&'
     else:
         url = rtok.callback + '?'
     url += 'oauth_token=%s&oauth_verifier=%s' % (
         rtok.api_key, rtok.validation_pin)
     redirect(url)
Beispiel #19
0
 def do_authorize(self, yes=None, no=None, oauth_token=None):
     security.require_authenticated()
     rtok = M.OAuthRequestToken.query.get(api_key=oauth_token)
     if no:
         rtok.delete()
         flash('%s NOT AUTHORIZED' % rtok.consumer_token.name, 'error')
         redirect('/auth/oauth/')
     if rtok.callback == 'oob':
         rtok.validation_pin = h.nonce(6)
         return dict(rtok=rtok)
     rtok.validation_pin = h.nonce(20)
     if '?' in rtok.callback:
         url = rtok.callback + '&'
     else:
         url = rtok.callback + '?'
     url += 'oauth_token=%s&oauth_verifier=%s' % (
         rtok.api_key, rtok.validation_pin)
     redirect(url)
Beispiel #20
0
 def update_mounts(self, subproject=None, tool=None, new=None, **kw):
     if subproject is None: subproject = []
     if tool is None: tool = []
     for sp in subproject:
         p = M.Project.query.get(shortname=sp['shortname'],
                                 neighborhood_id=c.project.neighborhood_id)
         if sp.get('delete'):
             require_access(c.project, 'admin')
             M.AuditLog.log('delete subproject %s', sp['shortname'])
             h.log_action(log, 'delete subproject').info(
                 'delete subproject %s', sp['shortname'],
                 meta=dict(name=sp['shortname']))
             p.removal = 'deleted'
             plugin.ProjectRegistrationProvider.get().delete_project(p, c.user)
         elif not new:
             M.AuditLog.log('update subproject %s', sp['shortname'])
             p.name = sp['name']
             p.ordinal = int(sp['ordinal'])
     for p in tool:
         if p.get('delete'):
             require_access(c.project, 'admin')
             M.AuditLog.log('uninstall tool %s', p['mount_point'])
             h.log_action(log, 'uninstall tool').info(
                 'uninstall tool %s', p['mount_point'],
                 meta=dict(mount_point=p['mount_point']))
             c.project.uninstall_app(p['mount_point'])
         elif not new:
             M.AuditLog.log('update tool %s', p['mount_point'])
             options = c.project.app_config(p['mount_point']).options
             options.mount_label = p['mount_label']
             options.ordinal = int(p['ordinal'])
     try:
         if new and new.get('install'):
             ep_name = new.get('ep_name', None)
             if not ep_name:
                 require_access(c.project, 'create')
                 mount_point = new['mount_point'].lower() or h.nonce()
                 M.AuditLog.log('create subproject %s', mount_point)
                 h.log_action(log, 'create subproject').info(
                     'create subproject %s', mount_point,
                     meta=dict(mount_point=mount_point,name=new['mount_label']))
                 sp = c.project.new_subproject(mount_point)
                 sp.name = new['mount_label']
                 sp.ordinal = int(new['ordinal'])
             else:
                 require_access(c.project, 'admin')
                 mount_point = new['mount_point'] or ep_name
                 M.AuditLog.log('install tool %s', mount_point)
                 h.log_action(log, 'install tool').info(
                     'install tool %s', mount_point,
                     meta=dict(tool_type=ep_name, mount_point=mount_point, mount_label=new['mount_label']))
                 c.project.install_app(ep_name, mount_point, mount_label=new['mount_label'], ordinal=new['ordinal'])
     except forge_exc.ForgeError, exc:
         flash('%s: %s' % (exc.__class__.__name__, exc.args[0]),
               'error')
 def make_slugs(cls, parent=None, timestamp=None):
     part = h.nonce()
     if timestamp is None:
         timestamp = datetime.utcnow()
     dt = timestamp.strftime("%Y%m%d%H%M%S")
     slug = part
     full_slug = dt + ":" + part
     if parent:
         return (parent.slug + "/" + slug, parent.full_slug + "/" + full_slug)
     else:
         return slug, full_slug
Beispiel #22
0
 def make_slugs(cls, parent=None, timestamp=None):
     part = h.nonce()
     if timestamp is None:
         timestamp = datetime.utcnow()
     dt = timestamp.strftime('%Y%m%d%H%M%S')
     slug = part
     full_slug = dt + ':' + part
     if parent:
         return (parent.slug + '/' + slug,
                 parent.full_slug + '/' + full_slug)
     else:
         return slug, full_slug
Beispiel #23
0
    def password_recovery_hash(self, email=None, **kw):
        provider = plugin.AuthenticationProvider.get(request)
        if not provider.forgotten_password_process:
            raise wexc.HTTPNotFound()
        if not email:
            redirect("/")

        user_record = M.User.by_email_address(email)
        allow_non_primary_email_reset = asbool(config.get("auth.allow_non_primary_email_password_reset", True))

        if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
            flash("Enter email in correct format!", "error")
            redirect("/auth/forgotten_password")

        if not allow_non_primary_email_reset:
            message = (
                "If the given email address is on record, "
                "a password reset email has been sent to the account's primary email address."
            )
            email_record = M.EmailAddress.get(
                email=provider.get_primary_email_address(user_record=user_record), confirmed=True
            )
        else:
            message = "A password reset email has been sent, if the given email address is on record in our system."
            email_record = M.EmailAddress.get(email=email, confirmed=True)

        if user_record and email_record and email_record.confirmed:
            hash = h.nonce(42)
            user_record.set_tool_data(
                "AuthPasswordReset",
                hash=hash,
                hash_expiry=datetime.datetime.utcnow()
                + datetime.timedelta(seconds=int(config.get("auth.recovery_hash_expiry_period", 600))),
            )

            log.info("Sending password recovery link to %s", email_record.email)
            subject = "%s Password recovery" % config["site_name"]
            text = g.jinja2_env.get_template("allura:templates/mail/forgot_password.txt").render(
                dict(user=user_record, config=config, hash=hash)
            )

            allura.tasks.mail_tasks.sendsimplemail.post(
                toaddr=email_record.email,
                fromaddr=config["forgemail.return_path"],
                reply_to=config["forgemail.return_path"],
                subject=subject,
                message_id=h.gen_message_id(),
                text=text,
            )
        h.auditlog_user("Password recovery link sent to: %s", email, user=user_record)
        flash(message)
        redirect("/")
Beispiel #24
0
    def password_recovery_hash(self, email=None, **kw):
        provider = plugin.AuthenticationProvider.get(request)
        if not provider.forgotten_password_process:
            raise wexc.HTTPNotFound()
        if not email:
            redirect('/')

        user_record = M.User.by_email_address(email)
        allow_non_primary_email_reset = asbool(config.get('auth.allow_non_primary_email_password_reset', True))

        if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
            flash('Enter email in correct format!','error')
            redirect('/auth/forgotten_password')

        if not allow_non_primary_email_reset:
            message = 'If the given email address is on record, '\
                      'a password reset email has been sent to the account\'s primary email address.'
            email_record = M.EmailAddress.get(email=provider.get_primary_email_address(user_record=user_record),
                                                    confirmed=True)
        else:
            message = 'A password reset email has been sent, if the given email address is on record in our system.'
            email_record = M.EmailAddress.get(email=email, confirmed=True)

        if user_record and email_record and email_record.confirmed:
            hash = h.nonce(42)
            user_record.set_tool_data('AuthPasswordReset',
                                      hash=hash,
                                      hash_expiry=datetime.datetime.utcnow() +
                                      datetime.timedelta(seconds=int(config.get('auth.recovery_hash_expiry_period', 600))))

            log.info('Sending password recovery link to %s', email_record.email)
            subject = '%s Password recovery' % config['site_name']
            text = g.jinja2_env.get_template('allura:templates/mail/forgot_password.txt').render(dict(
                user=user_record,
                config=config,
                hash=hash,
            ))

            allura.tasks.mail_tasks.sendsimplemail.post(
                toaddr=email_record.email,
                fromaddr=config['forgemail.return_path'],
                reply_to=config['forgemail.return_path'],
                subject=subject,
                message_id=h.gen_message_id(),
                text=text)
        h.auditlog_user('Password recovery link sent to: %s', email, user=user_record)
        flash(message)
        redirect('/')
class ApiToken(MappedClass, ApiAuthMixIn):
    class __mongometa__:
        name='api_token'
        session = main_orm_session
        unique_indexes = [ 'user_id' ]

    _id = FieldProperty(S.ObjectId)
    user_id = ForeignIdProperty('User')
    api_key = FieldProperty(str, if_missing=lambda:h.nonce(20))
    secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)

    user = RelationProperty('User')

    @classmethod
    def get(cls, api_key):
        return cls.query.get(api_key=api_key)
Beispiel #26
0
    def send_password_reset_email(self, email_address=None, subject_tmpl=u'{site_name} Password recovery'):
        if email_address is None:
            email_address = self.get_pref('email_address')
        hash = h.nonce(42)
        self.set_tool_data('AuthPasswordReset',
                           hash=hash,
                           hash_expiry=datetime.utcnow() +
                                       timedelta(seconds=int(config.get('auth.recovery_hash_expiry_period', 600))))

        log.info('Sending password recovery link to %s', email_address)
        subject = subject_tmpl.format(site_name=config['site_name'])
        text = g.jinja2_env.get_template('allura:templates/mail/forgot_password.txt').render(dict(
            user=self,
            config=config,
            hash=hash,
        ))
        allura.tasks.mail_tasks.send_system_mail_to_user(email_address, subject, text)
Beispiel #27
0
 def test_bearer_token_valid(self, request):
     user = M.User.by_username("test-admin")
     consumer_token = M.OAuthConsumerToken(name="foo", description="foo app")
     request_token = M.OAuthRequestToken(
         consumer_token_id=consumer_token._id,
         user_id=user._id,
         callback="manual",
         validation_pin=h.nonce(20),
         is_bearer=True,
     )
     access_token = M.OAuthAccessToken(
         consumer_token_id=consumer_token._id, request_token_id=request_token._id, user_id=user._id, is_bearer=True
     )
     ThreadLocalODMSession.flush_all()
     request.params = {"access_token": access_token.api_key}
     request.scheme = "https"
     r = self.api_post("/rest/p/test/wiki", access_token="foo")
     assert_equal(r.status_int, 200)
Beispiel #28
0
class OAuthToken(MappedClass):
    class __mongometa__:
        session = main_orm_session
        name = 'oauth_token'
        indexes = ['api_key']
        polymorphic_on = 'type'
        polymorphic_identity = None

    _id = FieldProperty(S.ObjectId)
    type = FieldProperty(str)
    api_key = FieldProperty(str, if_missing=lambda: h.nonce(20))
    secret_key = FieldProperty(str, if_missing=h.cryptographic_nonce)

    def to_string(self):
        return oauth.Token(self.api_key, self.secret_key).to_string()

    def as_token(self):
        return oauth.Token(self.api_key, self.secret_key)
Beispiel #29
0
class Thread(Artifact, ActivityObject):
    class __mongometa__:
        name = 'thread'
        indexes = [
            ('artifact_id', ),
            ('ref_id', ),
            (('app_config_id', pymongo.ASCENDING),
             ('last_post_date', pymongo.DESCENDING), ('mod_date',
                                                      pymongo.DESCENDING)),
            ('discussion_id', ),
        ]

    type_s = 'Thread'

    _id = FieldProperty(str, if_missing=lambda: h.nonce(8))
    discussion_id = ForeignIdProperty(Discussion)
    ref_id = ForeignIdProperty('ArtifactReference')
    subject = FieldProperty(str, if_missing='')
    num_replies = FieldProperty(int, if_missing=0)
    num_views = FieldProperty(int, if_missing=0)
    subscriptions = FieldProperty({str: bool})
    first_post_id = ForeignIdProperty('Post')
    last_post_date = FieldProperty(datetime, if_missing=datetime(1970, 1, 1))
    artifact_reference = FieldProperty(schema.Deprecated)
    artifact_id = FieldProperty(schema.Deprecated)

    discussion = RelationProperty(Discussion)
    posts = RelationProperty('Post', via='thread_id')
    first_post = RelationProperty('Post', via='first_post_id')
    ref = RelationProperty('ArtifactReference')

    def __json__(self, limit=None, page=None):
        return dict(
            _id=self._id,
            discussion_id=str(self.discussion_id),
            subject=self.subject,
            posts=[
                dict(slug=p.slug,
                     text=p.text,
                     subject=p.subject,
                     author=p.author().username,
                     timestamp=p.timestamp,
                     attachments=[
                         dict(bytes=attach.length, url=h.absurl(attach.url()))
                         for attach in p.attachments
                     ])
                for p in self.query_posts(
                    status='ok', style='chronological', limit=limit, page=page)
            ])

    @property
    def activity_name(self):
        return 'thread %s' % self.subject

    def parent_security_context(self):
        return self.discussion

    @classmethod
    def new(cls, **props):
        '''Creates a new Thread instance, ensuring a unique _id.'''
        for i in range(5):
            try:
                thread = cls(**props)
                session(thread).flush(thread)
                return thread
            except DuplicateKeyError as err:
                log.warning(
                    'Got DuplicateKeyError: attempt #%s, trying again. %s', i,
                    err)
                if i == 4:
                    raise
                session(thread).expunge(thread)
                continue

    @classmethod
    def discussion_class(cls):
        return cls.discussion.related

    @classmethod
    def post_class(cls):
        return cls.posts.related

    @classmethod
    def attachment_class(cls):
        return DiscussionAttachment

    @property
    def artifact(self):
        if self.ref is None:
            return self.discussion
        return self.ref.artifact

    # Use wisely - there's .num_replies also
    @property
    def post_count(self):
        return Post.query.find(
            dict(discussion_id=self.discussion_id,
                 thread_id=self._id,
                 status={'$in': ['ok', 'pending']})).count()

    def primary(self):
        if self.ref is None:
            return self
        return self.ref.artifact

    def add_post(self, **kw):
        """Helper function to avoid code duplication."""
        p = self.post(**kw)
        p.commit(update_stats=False)
        self.num_replies += 1
        if not self.first_post:
            self.first_post_id = p._id
        link = None
        if self.app.tool_label.lower() == 'tickets':
            link = p.url_paginated()
        if self.ref:
            Feed.post(self.primary(),
                      title=p.subject,
                      description=p.text,
                      link=link)
        return p

    def is_spam(self, post):
        if c.user in c.project.users_with_role('Admin', 'Developer'):
            return False
        else:
            return g.spam_checker.check(post.text, artifact=post, user=c.user)

    def post(self,
             text,
             message_id=None,
             parent_id=None,
             timestamp=None,
             ignore_security=False,
             **kw):
        if not ignore_security:
            require_access(self, 'post')
        if self.ref_id and self.artifact:
            self.artifact.subscribe()
        if message_id is None:
            message_id = h.gen_message_id()
        parent = parent_id and self.post_class().query.get(_id=parent_id)
        slug, full_slug = self.post_class().make_slugs(parent, timestamp)
        kwargs = dict(discussion_id=self.discussion_id,
                      full_slug=full_slug,
                      slug=slug,
                      thread_id=self._id,
                      parent_id=parent_id,
                      text=text,
                      status='pending')
        if timestamp is not None:
            kwargs['timestamp'] = timestamp
        if message_id is not None:
            kwargs['_id'] = message_id
        post = self.post_class()(**kwargs)
        if ignore_security or not self.is_spam(post) and has_access(
                self, 'unmoderated_post')():
            log.info('Auto-approving message from %s', c.user.username)
            file_info = kw.get('file_info', None)
            post.approve(file_info, notify=kw.get('notify', True))
        else:
            self.notify_moderators(post)
        return post

    def notify_moderators(self, post):
        ''' Notify moderators that a post needs approval [#2963] '''
        artifact = self.artifact or self
        subject = '[%s:%s] Moderation action required' % (
            c.project.shortname, c.app.config.options.mount_point)
        author = post.author()
        url = self.discussion_class().query.get(_id=self.discussion_id).url()
        text = ('The following submission requires approval at %s before '
                'it can be approved for posting:\n\n%s' %
                (h.absurl(url + 'moderate'), post.text))
        n = Notification(ref_id=artifact.index_id(),
                         topic='message',
                         link=artifact.url(),
                         _id=artifact.url() + post._id,
                         from_address=str(author._id)
                         if author != User.anonymous() else None,
                         reply_to_address=u'*****@*****.**',
                         subject=subject,
                         text=text,
                         in_reply_to=post.parent_id,
                         author_id=author._id,
                         pubdate=datetime.utcnow())
        users = self.app_config.project.users()
        for u in users:
            if (has_access(self, 'moderate', u)
                    and Mailbox.subscribed(user_id=u._id,
                                           app_config_id=post.app_config_id)):
                n.send_direct(str(u._id))

    def update_stats(self):
        self.num_replies = self.post_class().query.find(
            dict(thread_id=self._id, status='ok')).count() - 1

    @property
    def last_post(self):
        q = self.post_class().query.find(dict(thread_id=self._id)).sort(
            'timestamp', pymongo.DESCENDING)
        return q.first()

    def create_post_threads(self, posts):
        result = []
        post_index = {}
        for p in sorted(posts, key=lambda p: p.full_slug):
            pi = dict(post=p, children=[])
            post_index[p._id] = pi
            if p.parent_id in post_index:
                post_index[p.parent_id]['children'].append(pi)
            else:
                result.append(pi)
        return result

    def query_posts(self,
                    page=None,
                    limit=None,
                    timestamp=None,
                    style='threaded',
                    status=None):
        if timestamp:
            terms = dict(discussion_id=self.discussion_id,
                         thread_id=self._id,
                         status={'$in': ['ok', 'pending']},
                         timestamp=timestamp)
        else:
            terms = dict(discussion_id=self.discussion_id,
                         thread_id=self._id,
                         status={'$in': ['ok', 'pending']})
        if status:
            terms['status'] = status
        q = self.post_class().query.find(terms)
        if style == 'threaded':
            q = q.sort('full_slug')
        else:
            q = q.sort('timestamp')
        if limit is not None:
            limit = int(limit)
            if page is not None:
                q = q.skip(page * limit)
            q = q.limit(limit)
        return q

    def find_posts(self,
                   page=None,
                   limit=None,
                   timestamp=None,
                   style='threaded'):
        return self.query_posts(page=page,
                                limit=limit,
                                timestamp=timestamp,
                                style=style).all()

    def url(self):
        # Can't use self.discussion because it might change during the req
        discussion = self.discussion_class().query.get(_id=self.discussion_id)
        return discussion.url() + 'thread/' + str(self._id) + '/'

    def shorthand_id(self):
        return self._id

    def index(self):
        result = Artifact.index(self)
        result.update(title=self.subject or '(no subject)',
                      name_s=self.subject,
                      views_i=self.num_views,
                      text=self.subject)
        return result

    def _get_subscription(self):
        return self.subscriptions.get(str(c.user._id))

    def _set_subscription(self, value):
        if value:
            self.subscriptions[str(c.user._id)] = True
        else:
            self.subscriptions.pop(str(c.user._id), None)

    subscription = property(_get_subscription, _set_subscription)

    def delete(self):
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.delete()
        self.attachment_class().remove(dict(thread_id=self._id))
        super(Thread, self).delete()

    def spam(self):
        """Mark this thread as spam."""
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.spam()
Beispiel #30
0
class Feed(MappedClass):
    """
    Used to generate rss/atom feeds.  This does not need to be extended;
    all feed items go into the same collection
    """
    class __mongometa__:
        session = project_orm_session
        name = 'artifact_feed'
        indexes = [
            'pubdate',
            ('artifact_ref.project_id', 'artifact_ref.mount_point'),
            (('ref_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            (('project_id', pymongo.ASCENDING),
             ('app_config_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            'author_link',  # used in ext/user_profile/user_main.py for user feeds
        ]

    _id = FieldProperty(S.ObjectId)
    ref_id = ForeignIdProperty('ArtifactReference')
    neighborhood_id = ForeignIdProperty('Neighborhood')
    project_id = ForeignIdProperty('Project')
    app_config_id = ForeignIdProperty('AppConfig')
    tool_name=FieldProperty(str)
    title=FieldProperty(str)
    link=FieldProperty(str)
    pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)
    description = FieldProperty(str)
    unique_id = FieldProperty(str, if_missing=lambda:h.nonce(40))
    author_name = FieldProperty(str, if_missing=lambda:c.user.get_pref('display_name') if hasattr(c, 'user') else None)
    author_link = FieldProperty(str, if_missing=lambda:c.user.url() if hasattr(c, 'user') else None)
    artifact_reference = FieldProperty(S.Deprecated)


    @classmethod
    def post(cls, artifact, title=None, description=None, author=None, author_link=None, author_name=None, pubdate=None, link=None, **kw):
        """
        Create a Feed item.  Returns the item.
        But if anon doesn't have read access, create does not happen and None is returned
        """
        # TODO: fix security system so we can do this correctly and fast
        from allura import model as M
        anon = M.User.anonymous()
        if not security.has_access(artifact, 'read', user=anon):
            return
        if not security.has_access(c.project, 'read', user=anon):
            return
        idx = artifact.index()
        if author is None:
            author = c.user
        if author_name is None:
            author_name = author.get_pref('display_name')
        if title is None:
            title='%s modified by %s' % (h.get_first(idx, 'title'), author_name)
        if description is None: description = title
        if pubdate is None:
            pubdate = datetime.utcnow()
        if link is None:
            link=artifact.url()
        item = cls(
            ref_id=artifact.index_id(),
            neighborhood_id=artifact.app_config.project.neighborhood_id,
            project_id=artifact.app_config.project_id,
            app_config_id=artifact.app_config_id,
            tool_name=artifact.app_config.tool_name,
            title=title,
            description=g.markdown.convert(description),
            link=link,
            pubdate=pubdate,
            author_name=author_name,
            author_link=author_link or author.url())
        unique_id = kw.pop('unique_id', None)
        if unique_id:
            item.unique_id = unique_id
        return item

    @classmethod
    def feed(cls, q, feed_type, title, link, description,
             since=None, until=None, offset=None, limit=None):
        "Produces webhelper.feedgenerator Feed"
        d = dict(title=title, link=h.absurl(link), description=description, language=u'en')
        if feed_type == 'atom':
            feed = FG.Atom1Feed(**d)
        elif feed_type == 'rss':
            feed = FG.Rss201rev2Feed(**d)
        query = defaultdict(dict)
        query.update(q)
        if since is not None:
            query['pubdate']['$gte'] = since
        if until is not None:
            query['pubdate']['$lte'] = until
        cur = cls.query.find(query)
        cur = cur.sort('pubdate', pymongo.DESCENDING)
        if limit is None: limit = 10
        query = cur.limit(limit)
        if offset is not None: query = cur.offset(offset)
        for r in cur:
            feed.add_item(title=r.title,
                          link=h.absurl(r.link.encode('utf-8')),
                          pubdate=r.pubdate,
                          description=r.description,
                          unique_id=h.absurl(r.unique_id),
                          author_name=r.author_name,
                          author_link=h.absurl(r.author_link))
        return feed
class Notification(MappedClass):
    class __mongometa__:
        session = main_orm_session
        name = 'notification'
        indexes = [
            ('neighborhood_id', 'tool_name', 'pubdate'),
            ('author_id',
             ),  # used in ext/user_profile/user_main.py for user feeds
        ]

    _id = FieldProperty(str, if_missing=h.gen_message_id)

    # Classify notifications
    neighborhood_id = ForeignIdProperty(
        'Neighborhood', if_missing=lambda: c.project.neighborhood._id)
    project_id = ForeignIdProperty('Project', if_missing=lambda: c.project._id)
    app_config_id = ForeignIdProperty('AppConfig',
                                      if_missing=lambda: c.app.config._id)
    tool_name = FieldProperty(str, if_missing=lambda: c.app.config.tool_name)
    ref_id = ForeignIdProperty('ArtifactReference')
    topic = FieldProperty(str)
    unique_id = FieldProperty(str, if_missing=lambda: h.nonce(40))

    # Notification Content
    in_reply_to = FieldProperty(str)
    from_address = FieldProperty(str)
    reply_to_address = FieldProperty(str)
    subject = FieldProperty(str)
    text = FieldProperty(str)
    link = FieldProperty(str)
    author_id = ForeignIdProperty('User')
    feed_meta = FieldProperty(S.Deprecated)
    artifact_reference = FieldProperty(S.Deprecated)
    pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)

    ref = RelationProperty('ArtifactReference')

    view = jinja2.Environment(
        loader=jinja2.PackageLoader('allura', 'templates'))

    def author(self):
        return User.query.get(_id=self.author_id) or User.anonymous()

    @classmethod
    def post(cls, artifact, topic, **kw):
        '''Create a notification and  send the notify message'''
        import allura.tasks.notification_tasks
        n = cls._make_notification(artifact, topic, **kw)
        if n:
            allura.tasks.notification_tasks.notify.post(
                n._id, artifact.index_id(), topic)
        return n

    @classmethod
    def post_user(cls, user, artifact, topic, **kw):
        '''Create a notification and deliver directly to a user's flash
    mailbox'''
        try:
            mbox = Mailbox(user_id=user._id,
                           is_flash=True,
                           project_id=None,
                           app_config_id=None)
            session(mbox).flush(mbox)
        except pymongo.errors.DuplicateKeyError:
            session(mbox).expunge(mbox)
            mbox = Mailbox.query.get(user_id=user._id, is_flash=True)
        n = cls._make_notification(artifact, topic, **kw)
        if n:
            mbox.queue.append(n._id)
        return n

    @classmethod
    def _make_notification(cls, artifact, topic, **kwargs):
        from allura.model import Project
        idx = artifact.index()
        subject_prefix = '[%s:%s] ' % (c.project.shortname,
                                       c.app.config.options.mount_point)
        if topic == 'message':
            post = kwargs.pop('post')
            text = post.text
            file_info = kwargs.pop('file_info', None)
            if file_info is not None:
                file_info.file.seek(0, 2)
                bytecount = file_info.file.tell()
                file_info.file.seek(0)
                text = "%s\n\n\nAttachment: %s (%s; %s)" % (
                    text, file_info.filename, h.do_filesizeformat(bytecount),
                    file_info.type)

            subject = post.subject or ''
            if post.parent_id and not subject.lower().startswith('re:'):
                subject = 'Re: ' + subject
            author = post.author()
            d = dict(
                _id=artifact.url() + post._id,
                from_address=str(author._id)
                if author != User.anonymous() else None,
                reply_to_address='"%s" <%s>' %
                (subject_prefix,
                 getattr(artifact, 'email_address', u'*****@*****.**')),
                subject=subject_prefix + subject,
                text=text,
                in_reply_to=post.parent_id,
                author_id=author._id,
                pubdate=datetime.utcnow())
        else:
            subject = kwargs.pop(
                'subject', '%s modified by %s' %
                (idx['title_s'], c.user.get_pref('display_name')))
            reply_to = '"%s" <%s>' % (idx['title_s'],
                                      getattr(artifact, 'email_address',
                                              u'*****@*****.**'))
            d = dict(from_address=reply_to,
                     reply_to_address=reply_to,
                     subject=subject_prefix + subject,
                     text=kwargs.pop('text', subject),
                     author_id=c.user._id,
                     pubdate=datetime.utcnow())
            if c.user.get_pref('email_address'):
                d['from_address'] = '"%s" <%s>' % (c.user.get_pref(
                    'display_name'), c.user.get_pref('email_address'))
            elif c.user.email_addresses:
                d['from_address'] = '"%s" <%s>' % (
                    c.user.get_pref('display_name'), c.user.email_addresses[0])
        if not d.get('text'):
            d['text'] = ''
        try:
            ''' Add addional text to the notification e-mail based on the artifact type '''
            template = cls.view.get_template('mail/' + artifact.type_s +
                                             '.txt')
            d['text'] += template.render(
                dict(c=c, g=g, config=config, data=artifact))
        except jinja2.TemplateNotFound:
            pass
        except:
            ''' Catch any errors loading or rendering the template,
            but the notification still gets sent if there is an error
            '''
            log.warn('Could not render notification template %s' %
                     artifact.type_s,
                     exc_info=True)

        assert d['reply_to_address'] is not None
        project = Project.query.get(_id=d.get('project_id', c.project._id))
        if project.notifications_disabled:
            log.info(
                'Notifications disabled for project %s, not sending %s(%r)',
                project.shortname, topic, artifact)
            return None
        n = cls(ref_id=artifact.index_id(),
                topic=topic,
                link=kwargs.pop('link', artifact.url()),
                **d)
        return n

    @classmethod
    def feed(cls,
             q,
             feed_type,
             title,
             link,
             description,
             since=None,
             until=None,
             offset=None,
             limit=None):
        """Produces webhelper.feedgenerator Feed"""
        d = dict(title=title,
                 link=h.absurl(link),
                 description=description,
                 language=u'en')
        if feed_type == 'atom':
            feed = FG.Atom1Feed(**d)
        elif feed_type == 'rss':
            feed = FG.Rss201rev2Feed(**d)
        query = defaultdict(dict)
        query.update(q)
        if since is not None:
            query['pubdate']['$gte'] = since
        if until is not None:
            query['pubdate']['$lte'] = until
        cur = cls.query.find(query)
        cur = cur.sort('pubdate', pymongo.DESCENDING)
        if limit is None: limit = 10
        query = cur.limit(limit)
        if offset is not None: query = cur.offset(offset)
        for r in cur:
            feed.add_item(title=r.subject,
                          link=h.absurl(r.link.encode('utf-8')),
                          pubdate=r.pubdate,
                          description=r.text,
                          unique_id=r.unique_id,
                          author_name=r.author().display_name,
                          author_link=h.absurl(r.author().url()))
        return feed

    def footer(self):
        template = self.view.get_template('mail/footer.txt')
        return template.render(
            dict(notification=self,
                 prefix=config.get('forgemail.url',
                                   'https://sourceforge.net')))

    def send_simple(self, toaddr):
        allura.tasks.mail_tasks.sendsimplemail.post(
            toaddr=toaddr,
            fromaddr=self.from_address,
            reply_to=self.reply_to_address,
            subject=self.subject,
            message_id=self._id,
            in_reply_to=self.in_reply_to,
            text=(self.text or '') + self.footer())

    def send_direct(self, user_id):
        user = User.query.get(_id=ObjectId(user_id))
        artifact = self.ref.artifact
        # Don't send if user doesn't have read perms to the artifact
        if user and artifact and \
                not security.has_access(artifact, 'read', user)():
            log.debug("Skipping notification - User %s doesn't have read "
                      "access to artifact %s" % (user_id, str(self.ref_id)))
            return
        allura.tasks.mail_tasks.sendmail.post(destinations=[str(user_id)],
                                              fromaddr=self.from_address,
                                              reply_to=self.reply_to_address,
                                              subject=self.subject,
                                              message_id=self._id,
                                              in_reply_to=self.in_reply_to,
                                              text=(self.text or '') +
                                              self.footer())

    @classmethod
    def send_digest(self,
                    user_id,
                    from_address,
                    subject,
                    notifications,
                    reply_to_address=None):
        if not notifications: return
        # Filter out notifications for which the user doesn't have read
        # permissions to the artifact.
        user = User.query.get(_id=ObjectId(user_id))
        artifact = self.ref.artifact

        def perm_check(notification):
            return not (user and artifact) or \
                    security.has_access(artifact, 'read', user)()

        notifications = filter(perm_check, notifications)

        if reply_to_address is None:
            reply_to_address = from_address
        text = ['Digest of %s' % subject]
        for n in notifications:
            text.append('From: %s' % n.from_address)
            text.append('Subject: %s' % (n.subject or '(no subject)'))
            text.append('Message-ID: %s' % n._id)
            text.append('')
            text.append(n.text or '-no text-')
        text.append(n.footer())
        text = '\n'.join(text)
        allura.tasks.mail_tasks.sendmail.post(destinations=[str(user_id)],
                                              fromaddr=from_address,
                                              reply_to=reply_to_address,
                                              subject=subject,
                                              message_id=h.gen_message_id(),
                                              text=text)

    @classmethod
    def send_summary(self, user_id, from_address, subject, notifications):
        if not notifications: return
        text = ['Digest of %s' % subject]
        for n in notifications:
            text.append('From: %s' % n.from_address)
            text.append('Subject: %s' % (n.subject or '(no subject)'))
            text.append('Message-ID: %s' % n._id)
            text.append('')
            text.append(h.text.truncate(n.text or '-no text-', 128))
        text.append(n.footer())
        text = '\n'.join(text)
        allura.tasks.mail_tasks.sendmail.post(destinations=[str(user_id)],
                                              fromaddr=from_address,
                                              reply_to=from_address,
                                              subject=subject,
                                              message_id=h.gen_message_id(),
                                              text=text)
Beispiel #32
0
class Feed(MappedClass):

    """
    Used to generate rss/atom feeds.  This does not need to be extended;
    all feed items go into the same collection
    """
    class __mongometa__:
        session = project_orm_session
        name = 'artifact_feed'
        indexes = [
            'pubdate',
            ('artifact_ref.project_id', 'artifact_ref.mount_point'),
            (('ref_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            (('project_id', pymongo.ASCENDING),
             ('app_config_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
            # used in ext/user_profile/user_main.py for user feeds
            'author_link',
            # used in project feed
            (('project_id', pymongo.ASCENDING),
             ('pubdate', pymongo.DESCENDING)),
        ]

    _id = FieldProperty(S.ObjectId)
    ref_id = ForeignIdProperty('ArtifactReference')
    neighborhood_id = ForeignIdProperty('Neighborhood')
    project_id = ForeignIdProperty('Project')
    app_config_id = ForeignIdProperty('AppConfig')
    tool_name = FieldProperty(str)
    title = FieldProperty(str)
    link = FieldProperty(str)
    pubdate = FieldProperty(datetime, if_missing=datetime.utcnow)
    description = FieldProperty(str)
    description_cache = FieldProperty(MarkdownCache)
    unique_id = FieldProperty(str, if_missing=lambda: h.nonce(40))
    author_name = FieldProperty(str, if_missing=lambda: c.user.get_pref(
        'display_name') if hasattr(c, 'user') else None)
    author_link = FieldProperty(
        str, if_missing=lambda: c.user.url() if hasattr(c, 'user') else None)
    artifact_reference = FieldProperty(S.Deprecated)

    def clear_user_data(self):
        """ Redact author data """
        self.author_name = ""
        self.author_link = ""
        title_parts = self.title.partition(" modified by ")
        self.title = u"".join(title_parts[0:2]) + (u"<REDACTED>" if title_parts[2] else '')

    @classmethod
    def from_username(cls, username):
        return cls.query.find({'author_link': u"/u/{}/".format(username)}).all()

    @classmethod
    def has_access(cls, artifact):
        # Enable only for development.
        # return True
        from allura import model as M
        anon = M.User.anonymous()
        if not security.has_access(artifact, 'read', user=anon):
            return False
        if not security.has_access(c.project, 'read', user=anon):
            return False
        return True

    @classmethod
    def post(cls, artifact, title=None, description=None, author=None,
             author_link=None, author_name=None, pubdate=None, link=None, **kw):
        """
        Create a Feed item.  Returns the item.
        But if anon doesn't have read access, create does not happen and None is
        returned.
        """
        if not Feed.has_access(artifact):
            return
        idx = artifact.index()
        if author is None:
            author = c.user
        if author_name is None:
            author_name = author.get_pref('display_name')
        if title is None:
            title = '%s modified by %s' % (
                h.get_first(idx, 'title'), author_name)
        if description is None:
            description = title
        if pubdate is None:
            pubdate = datetime.utcnow()
        if link is None:
            link = artifact.url()
        item = cls(
            ref_id=artifact.index_id(),
            neighborhood_id=artifact.app_config.project.neighborhood_id,
            project_id=artifact.app_config.project_id,
            app_config_id=artifact.app_config_id,
            tool_name=artifact.app_config.tool_name,
            title=title,
            description=g.markdown.convert(description),
            link=link,
            pubdate=pubdate,
            author_name=author_name,
            author_link=author_link or author.url())
        unique_id = kw.pop('unique_id', None)
        if unique_id:
            item.unique_id = unique_id
        return item

    @classmethod
    def feed(cls, q, feed_type, title, link, description,
             since=None, until=None, page=None, limit=None):
        "Produces webhelper.feedgenerator Feed"
        d = dict(title=title, link=h.absurl(link),
                 description=description, language=u'en',
                 feed_url=request.url)
        if feed_type == 'atom':
            feed = FG.Atom1Feed(**d)
        elif feed_type == 'rss':
            feed = RssFeed(**d)
        limit, page = h.paging_sanitizer(limit or 10, page)
        query = defaultdict(dict)
        if callable(q):
            q = q(since, until, page, limit)
        query.update(q)
        if since is not None:
            query['pubdate']['$gte'] = since
        if until is not None:
            query['pubdate']['$lte'] = until
        cur = cls.query.find(query)
        cur = cur.sort('pubdate', pymongo.DESCENDING)
        cur = cur.limit(limit)
        cur = cur.skip(limit * page)
        for r in cur:
            feed.add_item(title=r.title,
                          link=h.absurl(r.link.encode('utf-8')),
                          pubdate=r.pubdate,
                          description=r.description,
                          unique_id=h.absurl(r.unique_id),
                          author_name=r.author_name,
                          author_link=h.absurl(r.author_link))
        return feed
Beispiel #33
0
    def _update_mounts(self, subproject=None, tool=None, new=None, **kw):
        '''

        Returns the new App or Subproject, if one was installed.
        Returns None otherwise.
        '''
        if subproject is None:
            subproject = []
        if tool is None:
            tool = []
        new_app = None
        for sp in subproject:
            p = M.Project.query.get(shortname=sp['shortname'],
                                    neighborhood_id=c.project.neighborhood_id)
            if sp.get('delete'):
                require_access(c.project, 'admin')
                M.AuditLog.log('delete subproject %s', sp['shortname'])
                p.removal = 'deleted'
                plugin.ProjectRegistrationProvider.get().delete_project(
                    p, c.user)
            elif not new:
                M.AuditLog.log('update subproject %s', sp['shortname'])
                p.name = sp['name']
                p.ordinal = int(sp['ordinal'])
        for p in tool:
            if p.get('delete'):
                require_access(c.project, 'admin')
                M.AuditLog.log('uninstall tool %s', p['mount_point'])
                c.project.uninstall_app(p['mount_point'])
            elif not new:
                M.AuditLog.log('update tool %s', p['mount_point'])
                options = c.project.app_config(p['mount_point']).options
                options.mount_label = p['mount_label']
                options.ordinal = int(p['ordinal'])
        if new and new.get('install'):
            ep_name = new.get('ep_name', None)
            if not ep_name:
                require_access(c.project, 'create')
                mount_point = new['mount_point'].lower() or h.nonce()
                M.AuditLog.log('create subproject %s', mount_point)
                sp = c.project.new_subproject(mount_point)
                sp.name = new['mount_label']
                if 'ordinal' in new:
                    sp.ordinal = int(new['ordinal'])
                else:
                    sp.ordinal = c.project.last_ordinal_value() + 1
                new_app = sp
            else:
                require_access(c.project, 'admin')
                installable_tools = AdminApp.installable_tools_for(c.project)
                if not ep_name.lower() in [t['name'].lower() for t in installable_tools]:
                    flash('Installation limit exceeded.', 'error')
                    return
                mount_point = new['mount_point'] or ep_name
                M.AuditLog.log('install tool %s', mount_point)
                App = g.entry_points['tool'][ep_name]
                # pass only options which app expects
                config_on_install = {
                    k: v for (k, v) in six.iteritems(kw)
                    if k in [o.name for o in App.options_on_install()]
                }
                new_app = c.project.install_app(
                    ep_name,
                    mount_point,
                    mount_label=new['mount_label'],
                    ordinal=int(new['ordinal']) if 'ordinal' in new else None,
                    **config_on_install)
        g.post_event('project_updated')
        g.post_event('project_menu_updated')
        return new_app
Beispiel #34
0
class Thread(Artifact, ActivityObject):

    class __mongometa__:
        name = 'thread'
        indexes = [
            ('artifact_id',),
            ('ref_id',),
            (('app_config_id', pymongo.ASCENDING),
             ('last_post_date', pymongo.DESCENDING),
             ('mod_date', pymongo.DESCENDING)),
            ('discussion_id',),
        ]
    type_s = 'Thread'

    _id = FieldProperty(str, if_missing=lambda: h.nonce(8))
    discussion_id = ForeignIdProperty(Discussion)
    ref_id = ForeignIdProperty('ArtifactReference')
    subject = FieldProperty(str, if_missing='')
    num_replies = FieldProperty(int, if_missing=0)
    num_views = FieldProperty(int, if_missing=0)
    subscriptions = FieldProperty({str: bool})
    first_post_id = ForeignIdProperty('Post')
    last_post_date = FieldProperty(datetime, if_missing=datetime(1970, 1, 1))
    artifact_reference = FieldProperty(schema.Deprecated)
    artifact_id = FieldProperty(schema.Deprecated)

    discussion = RelationProperty(Discussion)
    posts = RelationProperty('Post', via='thread_id')
    first_post = RelationProperty('Post', via='first_post_id')
    ref = RelationProperty('ArtifactReference')

    def should_update_index(self, old_doc, new_doc):
        """Skip index update if only `num_views` has changed.

        Value of `num_views` is updated whenever user loads thread page.
        This generates a lot of unnecessary `add_artifacts` tasks.
        """
        old_doc.pop('num_views', None)
        new_doc.pop('num_views', None)
        return old_doc != new_doc

    def attachment_for_export(self, page):
        return [dict(bytes=attach.length,
                     url=h.absurl(attach.url()),
                     path=os.path.join(
                         self.artifact.app_config.options.mount_point,
                         str(self.artifact._id),
                         self._id,
                         page.slug,
                         os.path.basename(attach.filename))
                     ) for attach in page.attachments]

    def attachments_for_json(self, page):
        return [dict(bytes=attach.length,
                     url=h.absurl(attach.url())) for attach in page.attachments]

    def __json__(self, limit=None, page=None, is_export=False):
        return dict(
            _id=self._id,
            discussion_id=str(self.discussion_id),
            subject=self.subject,
            limit=limit,
            page=page,
            posts=[dict(slug=p.slug,
                        text=p.text,
                        subject=p.subject,
                        author=p.author().username,
                        author_icon_url=h.absurl(p.author().icon_url()),
                        timestamp=p.timestamp,
                        last_edited=p.last_edit_date,
                        attachments=self.attachment_for_export(p) if is_export else self.attachments_for_json(p))
                   for p in self.query_posts(status='ok', style='chronological', limit=limit, page=page)
                   ]
        )

    @property
    def activity_name(self):
        return 'thread %s' % self.subject

    def parent_security_context(self):
        return self.discussion

    @classmethod
    def new(cls, **props):
        '''Creates a new Thread instance, ensuring a unique _id.'''
        for i in range(5):
            try:
                thread = cls(**props)
                session(thread).flush(thread)
                return thread
            except DuplicateKeyError as err:
                log.warning(
                    'Got DuplicateKeyError: attempt #%s, trying again. %s', i, err)
                if i == 4:
                    raise
                session(thread).expunge(thread)
                continue

    @classmethod
    def discussion_class(cls):
        return cls.discussion.related

    @classmethod
    def post_class(cls):
        return cls.posts.related

    @classmethod
    def attachment_class(cls):
        return DiscussionAttachment

    @property
    def artifact(self):
        # Threads attached to a wiki page, ticket, etc will have a .ref.artifact pointing to that WikiPage etc
        # Threads that are part of a forum will not have that
        if self.ref is None:
            return self.discussion
        return self.ref.artifact

    # Use wisely - there's .num_replies also
    @property
    def post_count(self):
        return Post.query.find(dict(
            discussion_id=self.discussion_id,
            thread_id=self._id,
            status={'$in': ['ok', 'pending']},
            deleted=False,
        )).count()

    def primary(self):
        if self.ref is None:
            return self
        return self.ref.artifact

    def post_to_feed(self, post):
        if post.status == 'ok':
            Feed.post(
                self.primary(),
                title=post.subject,
                description=post.text,
                link=post.url_paginated(),
                pubdate=post.mod_date,
            )

    def add_post(self, **kw):
        """Helper function to avoid code duplication."""
        p = self.post(**kw)
        p.commit(update_stats=False)
        session(self).flush(self)
        self.update_stats()
        if not self.first_post:
            self.first_post_id = p._id
        self.post_to_feed(p)
        return p

    def include_subject_in_spam_check(self, post):
        return (post.primary() == post  # only artifacts where the discussion is the main thing i.e. ForumPost
                and
                self.num_replies == 0)  # only first post in thread

    def is_spam(self, post):
        roles = [r.name for r in c.project.named_roles]
        spam_check_text = post.text
        if self.include_subject_in_spam_check(post):
            spam_check_text = self.subject + u'\n' + spam_check_text
        spammy = g.spam_checker.check(spam_check_text, artifact=post, user=c.user)
        if c.user in c.project.users_with_role(*roles):
            # always run the check, so it's logged.  But don't act on it for admins/developers of their own project
            return False
        else:
            return spammy

    def post(self, text, message_id=None, parent_id=None, notify=True,
             notification_text=None, timestamp=None, ignore_security=False,
             is_meta=False, **kw):
        if not ignore_security:
            require_access(self, 'post')
        if self.ref_id and self.artifact:
            self.artifact.subscribe()
        if message_id is None:
            message_id = h.gen_message_id()
        parent = parent_id and self.post_class().query.get(_id=parent_id)
        slug, full_slug = self.post_class().make_slugs(parent, timestamp)
        kwargs = dict(
            discussion_id=self.discussion_id,
            full_slug=full_slug,
            slug=slug,
            thread_id=self._id,
            parent_id=parent_id,
            text=text,
            status='pending',
            is_meta=is_meta)
        if timestamp is not None:
            kwargs['timestamp'] = timestamp
        if message_id is not None:
            kwargs['_id'] = message_id
        post = self.post_class()(**kwargs)

        # unmoderated post -> autoapprove
        # unmoderated post but is spammy -> don't approve it, it goes into moderation
        # moderated post -> moderation
        # moderated post but is spammy -> mark as spam
        spammy = self.is_spam(post)
        if ignore_security or (not spammy and has_access(self, 'unmoderated_post')):
            log.info('Auto-approving message from %s', c.user.username)
            file_info = kw.get('file_info', None)
            post.approve(file_info, notify=notify,
                         notification_text=notification_text)
        elif not has_access(self, 'unmoderated_post') and spammy:
            post.spam(submit_spam_feedback=False)  # no feedback since we're marking as spam automatically not manually
        else:
            self.notify_moderators(post)
        return post

    def notify_moderators(self, post):
        ''' Notify moderators that a post needs approval [#2963] '''
        artifact = self.artifact or self
        subject = '[%s:%s] Moderation action required' % (
            c.project.shortname, c.app.config.options.mount_point)
        author = post.author()
        url = self.discussion_class().query.get(_id=self.discussion_id).url()
        text = ('The following submission requires approval at %s before '
                'it can be approved for posting:\n\n%s'
                % (h.absurl(url + 'moderate'), post.text))
        n = Notification(
            ref_id=artifact.index_id(),
            topic='message',
            link=artifact.url(),
            _id=artifact.url() + post._id,
            from_address=str(author._id) if author != User.anonymous()
            else None,
            reply_to_address=g.noreply,
            subject=subject,
            text=text,
            in_reply_to=post.parent_id,
            author_id=author._id,
            pubdate=datetime.utcnow())
        users = self.app_config.project.users()
        for u in users:
            if (has_access(self, 'moderate', u)
                and Mailbox.subscribed(user_id=u._id,
                                       app_config_id=post.app_config_id)):
                    n.send_direct(str(u._id))

    def update_stats(self):
        self.num_replies = self.post_class().query.find(
            dict(thread_id=self._id, status='ok', deleted=False)).count()

    @LazyProperty
    def last_post(self):
        q = self.post_class().query.find(dict(
            thread_id=self._id,
            deleted=False,
        )).sort('timestamp', pymongo.DESCENDING)
        return q.first()

    def create_post_threads(self, posts):
        result = []
        post_index = {}
        for p in sorted(posts, key=lambda p: p.full_slug):
            pi = dict(post=p, children=[])
            post_index[p._id] = pi
            if p.parent_id in post_index:
                post_index[p.parent_id]['children'].append(pi)
            else:
                result.append(pi)
        return result

    def query_posts(self, page=None, limit=None,
                    timestamp=None, style='threaded', status=None):
        if timestamp:
            terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                         status={'$in': ['ok', 'pending']}, timestamp=timestamp)
        else:
            terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                         status={'$in': ['ok', 'pending']})
        if status:
            terms['status'] = status
        terms['deleted'] = False
        q = self.post_class().query.find(terms)
        if style == 'threaded':
            q = q.sort('full_slug')
        else:
            q = q.sort('timestamp')
        if limit is not None:
            limit = int(limit)
            if page is not None:
                q = q.skip(page * limit)
            q = q.limit(limit)
        return q

    def find_posts(self, page=None, limit=None, timestamp=None,
                   style='threaded'):
        return self.query_posts(page=page, limit=limit,
                                timestamp=timestamp, style=style).all()

    def url(self):
        # Can't use self.discussion because it might change during the req
        discussion = self.discussion_class().query.get(_id=self.discussion_id)
        return discussion.url() + 'thread/' + str(self._id) + '/'

    def shorthand_id(self):
        return self._id

    def index(self):
        result = Artifact.index(self)
        result.update(
            title=self.subject or '(no subject)',
            name_s=self.subject,
            views_i=self.num_views,
            text=self.subject)
        return result

    def delete(self):
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.delete()
        self.attachment_class().remove(dict(thread_id=self._id))
        super(Thread, self).delete()

    def spam(self):
        """Mark this thread as spam."""
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.spam()