Exemplo n.º 1
0
class TokenLog(db.Model):
    """TokenLog class is used for logging changes to access tokens."""
    __tablename__ = 'token_log'

    token_value = db.Column(db.String,
                            db.ForeignKey('token.value'),
                            primary_key=True)
    timestamp = db.Column(db.DateTime(timezone=True),
                          primary_key=True,
                          default=datetime.utcnow)
    action = db.Column(
        db.Enum(ACTION_DEACTIVATE,
                ACTION_CREATE,
                name='token_log_action_types'),
        primary_key=True,
    )
    user_id = db.Column(
        db.Integer,
        db.ForeignKey('user.id', ondelete="SET NULL", onupdate="CASCADE"))

    @classmethod
    def create_record(cls, access_token, action):
        user_id = current_user.id if current_user.is_authenticated() else None
        new_record = cls(
            token_value=access_token,
            action=action,
            user_id=user_id,
        )
        db.session.add(new_record)
        db.session.commit()
        return new_record

    @classmethod
    def list(cls, limit=None, offset=None):
        query = cls.query.order_by(cls.timestamp.desc())
        count = query.count()  # Total count should be calculated before limits
        if limit is not None:
            query = query.limit(limit)
        if offset is not None:
            query = query.offset(offset)
        return query.all(), count
Exemplo n.º 2
0
class User(db.Model, UserMixin):
    """User model is used for users of MetaBrainz services like Live Data Feed.

    Users are either commercial or non-commercial (see `is_commercial`). Their
    access to the API is determined by their `state` (active, pending, waiting,
    or rejected). All non-commercial users have active state by default, but
    commercial users need to be approved by one of the admins first.
    """
    __tablename__ = 'user'

    # Common columns used by both commercial and non-commercial users:
    id = db.Column(db.Integer, primary_key=True)
    is_commercial = db.Column(db.Boolean, nullable=False)
    musicbrainz_id = db.Column(
        db.Unicode, unique=True)  # MusicBrainz account that manages this user
    created = db.Column(db.DateTime(timezone=True), default=datetime.utcnow)
    state = db.Column(postgres.ENUM(STATE_ACTIVE,
                                    STATE_PENDING,
                                    STATE_WAITING,
                                    STATE_REJECTED,
                                    STATE_LIMITED,
                                    name='state_types'),
                      nullable=False)
    contact_name = db.Column(db.Unicode, nullable=False)
    contact_email = db.Column(db.Unicode, nullable=False)
    data_usage_desc = db.Column(db.UnicodeText)

    # Columns specific to commercial users:
    org_name = db.Column(db.Unicode)
    org_logo_url = db.Column(db.Unicode)
    website_url = db.Column(db.Unicode)
    api_url = db.Column(db.Unicode)
    org_desc = db.Column(db.UnicodeText)
    address_street = db.Column(db.Unicode)
    address_city = db.Column(db.Unicode)
    address_state = db.Column(db.Unicode)
    address_postcode = db.Column(db.Unicode)
    address_country = db.Column(db.Unicode)
    tier_id = db.Column(
        db.Integer,
        db.ForeignKey('tier.id', ondelete="SET NULL", onupdate="CASCADE"))
    amount_pledged = db.Column(db.Numeric(11, 2))

    # Administrative columns:
    good_standing = db.Column(db.Boolean, nullable=False, default=True)
    in_deadbeat_club = db.Column(db.Boolean, nullable=False, default=False)
    featured = db.Column(db.Boolean, nullable=False, default=False)

    tokens = db.relationship("Token", backref="owner", lazy="dynamic")
    token_log_records = db.relationship("TokenLog",
                                        backref="user",
                                        lazy="dynamic")

    def __unicode__(self):
        if self.is_commercial:
            return "%s (#%s)" % (self.org_name, self.id)
        else:
            if self.musicbrainz_id:
                return "#%s (MBID: %s)" % (self.id, self.musicbrainz_id)
            else:
                return str(self.id)

    @property
    def token(self):
        return Token.get(owner_id=self.id, is_active=True)

    @classmethod
    def add(cls, **kwargs):
        new_user = cls(
            is_commercial=kwargs.pop('is_commercial'),
            musicbrainz_id=kwargs.pop('musicbrainz_id'),
            contact_name=kwargs.pop('contact_name'),
            contact_email=kwargs.pop('contact_email'),
            data_usage_desc=kwargs.pop('data_usage_desc'),
            org_desc=kwargs.pop('org_desc', None),
            org_name=kwargs.pop('org_name', None),
            org_logo_url=kwargs.pop('org_logo_url', None),
            website_url=kwargs.pop('website_url', None),
            api_url=kwargs.pop('api_url', None),
            address_street=kwargs.pop('address_street', None),
            address_city=kwargs.pop('address_city', None),
            address_state=kwargs.pop('address_state', None),
            address_postcode=kwargs.pop('address_postcode', None),
            address_country=kwargs.pop('address_country', None),
            tier_id=kwargs.pop('tier_id', None),
            amount_pledged=kwargs.pop('amount_pledged', None),
        )
        new_user.state = STATE_ACTIVE if not new_user.is_commercial else STATE_PENDING
        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)
        db.session.add(new_user)
        db.session.commit()

        if new_user.is_commercial:
            send_user_signup_notification(new_user)

        return new_user

    @classmethod
    def get(cls, **kwargs):
        return cls.query.filter_by(**kwargs).first()

    @classmethod
    def get_all(cls, **kwargs):
        return cls.query.filter_by(**kwargs).all()

    @classmethod
    def get_all_commercial(cls, limit=None, offset=None):
        query = cls.query.filter(cls.is_commercial == True).order_by(
            cls.org_name)
        count = query.count()  # Total count should be calculated before limits
        if limit is not None:
            query = query.limit(limit)
        if offset is not None:
            query = query.offset(offset)
        return query.all(), count

    @classmethod
    def get_featured(cls, limit=None, **kwargs):
        """Get list of featured users which is randomly sorted.

        Args:
            limit: Max number of users to return.
            in_deadbeat_club: Returns only users from deadbeat club if set to True.
            with_logos: True if need only users with logo URLs specified, False if
                only users without logo URLs, None if it's irrelevant.
            tier_id: Returns only users from tier with a specified ID.

        Returns:
            List of users according to filters described above.
        """
        query = cls.query.filter(cls.featured == True)
        query = query.filter(
            cls.in_deadbeat_club == kwargs.pop('in_deadbeat_club', False))
        with_logos = kwargs.pop('with_logos', None)
        if with_logos:
            query = query.filter(cls.org_logo_url != None)
        tier_id = kwargs.pop('tier_id', None)
        if tier_id:
            query = query.filter(cls.tier_id == tier_id)
        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)
        return query.order_by(func.random()).limit(limit).all()

    @classmethod
    def search(cls, value):
        """Search users by their musicbrainz_id, org_name, contact_name,
        or contact_email.
        """
        query = cls.query.filter(
            or_(
                cls.musicbrainz_id.ilike('%' + value + '%'),
                cls.org_name.ilike('%' + value + '%'),
                cls.contact_name.ilike('%' + value + '%'),
                cls.contact_email.ilike('%' + value + '%'),
            ))
        return query.limit(20).all()

    def generate_token(self):
        """Generates new access token for this user."""
        if self.state == STATE_ACTIVE:
            return Token.generate_token(self.id)
        else:
            raise InactiveUserException(
                "Can't generate token for inactive user.")

    def update(self, **kwargs):
        contact_name = kwargs.pop('contact_name')
        if contact_name is not None:
            self.contact_name = contact_name
        contact_email = kwargs.pop('contact_email')
        if contact_email is not None:
            self.contact_email = contact_email
        if kwargs:
            raise TypeError('Unexpected **kwargs: %r' % kwargs)
        db.session.commit()

    def set_state(self, state):
        old_state = self.state
        self.state = state
        db.session.commit()
        if old_state != self.state:
            # TODO: Send additional info about new state.
            state_name = "ACTIVE" if self.state == STATE_ACTIVE else \
                         "REJECTED" if self.state == STATE_REJECTED else \
                         "PENDING" if self.state == STATE_PENDING else \
                         "WAITING" if self.state == STATE_WAITING else \
                         "LIMITED" if self.state == STATE_LIMITED else \
                         self.state
            send_mail(
                subject="[MetaBrainz] Your account has been updated",
                text=
                'State of your MetaBrainz account has been changed to "%s".' %
                state_name,
                recipients=[self.contact_email],
            )
Exemplo n.º 3
0
class AccessLog(db.Model):
    """Access log is used for tracking requests to the API.

    Each request needs to be logged. Logging is done to keep track of number of
    requests in a fixed time frame. If there is an unusual number of requests
    being made from different IP addresses in this time frame, action is taken.
    See implementation of this model for more details.
    """
    __tablename__ = 'access_log'

    token = db.Column(db.String,
                      db.ForeignKey('token.value'),
                      primary_key=True)
    timestamp = db.Column(db.DateTime(timezone=True),
                          primary_key=True,
                          default=datetime.utcnow)
    ip_address = db.Column(postgres.INET)

    @classmethod
    def create_record(cls, access_token, ip_address):
        """Creates new access log record with a current timestamp.

        It also checks if `DIFFERENT_IP_LIMIT` is exceeded within current time
        and `CLEANUP_RANGE_MINUTES`, alerts admins if that's the case.

        Args:
            access_token: Access token used to access the API.
            ip_address: IP access used to access the API.

        Returns:
            New access log record.
        """
        new_record = cls(
            token=access_token,
            ip_address=ip_address,
        )
        db.session.add(new_record)
        db.session.commit()

        # Checking if HOURLY_ALERT_THRESHOLD is exceeded
        count = cls.query \
            .distinct(cls.ip_address) \
            .filter(cls.timestamp > datetime.now(pytz.utc) - timedelta(minutes=CLEANUP_RANGE_MINUTES),
                    cls.token == access_token) \
            .count()
        if count > DIFFERENT_IP_LIMIT:
            msg = ("Hourly access threshold exceeded for token %s\n\n"
                   "This token has been used from %s different IP "
                   "addresses during the last %s minutes.") % \
                  (access_token, count, CLEANUP_RANGE_MINUTES)
            logging.info(msg)
            # Checking if notification for admins about this token abuse has
            # been sent in the last hour. This info is kept in cache.
            key = "alert_sent_%s" % access_token
            if not cache.get(key):
                send_mail(
                    subject="[MetaBrainz] Hourly access threshold exceeded",
                    recipients=current_app.config['NOTIFICATION_RECIPIENTS'],
                    text=msg,
                )
                cache.set(key, True, 3600)  # 1 hour

        return new_record

    @classmethod
    def remove_old_ip_addr_records(cls):
        cls.query. \
            filter(cls.timestamp < datetime.now(pytz.utc) - timedelta(minutes=CLEANUP_RANGE_MINUTES)). \
            update({cls.ip_address: None})
        db.session.commit()

    @classmethod
    def get_hourly_usage(cls, user_id=None):
        """Get information about API usage.

        Args:
            user_id: User ID that can be specified to get stats only for that account.

        Returns:
            List of <datetime, request count> tuples for every hour.
        """
        if not user_id:
            rows = db.engine.execute(
                'SELECT max("timestamp") as ts, count(*) '
                'FROM access_log '
                'GROUP BY extract(year from "timestamp"), extract(month from "timestamp"), '
                '         extract(day from "timestamp"), trunc(extract(hour from "timestamp")) '
                'ORDER BY ts')
        else:
            rows = db.engine.execute(
                'SELECT max(access_log."timestamp") as ts, count(access_log.*) '
                'FROM access_log '
                'JOIN token ON access_log.token = token.value '
                'JOIN "user" ON token.owner_id = "user".id '
                'WHERE "user".id = %s '
                'GROUP BY extract(year from "timestamp"), extract(month from "timestamp"), '
                '         extract(day from "timestamp"), trunc(extract(hour from "timestamp")) '
                'ORDER BY ts', (user_id, ))
        return [(r[0].replace(
            minute=0,
            second=0,
            microsecond=0,
            tzinfo=None,
        ), r[1]) for r in rows]

    @classmethod
    def active_user_count(cls):
        """Returns number of different users whose access has been logged in
        the last 24 hours.
        """
        return cls.query.join(Token).join(User) \
            .filter(cls.timestamp > datetime.now() - timedelta(days=1)) \
            .distinct(User.id).count()

    @classmethod
    def top_downloaders(cls, limit=None):
        """Generates list of most active users in the last 24 hours.

        Args:
            limit: Max number of items to return.

        Returns:
            List of <User, request count> pairs
        """
        query = db.session.query(User).join(Token).join(AccessLog) \
            .filter(cls.timestamp > datetime.now() - timedelta(days=1)) \
            .add_columns(func.count("AccessLog.*").label("count")).group_by(User.id) \
            .order_by("count DESC")
        if limit:
            query = query.limit(limit)
        return query.all()
Exemplo n.º 4
0
class Token(db.Model):
    __tablename__ = 'token'

    value = db.Column(db.String, primary_key=True)
    is_active = db.Column(db.Boolean, nullable=False, default=True)
    owner_id = db.Column(
        db.Integer,
        db.ForeignKey('user.id', ondelete="SET NULL", onupdate="CASCADE"))
    created = db.Column(db.DateTime(timezone=True), default=datetime.utcnow)

    log_records = db.relationship(TokenLog, backref="token", lazy="dynamic")

    @classmethod
    def get(cls, **kwargs):
        return cls.query.filter_by(**kwargs).first()

    @classmethod
    def get_all(cls, **kwargs):
        return cls.query.filter_by(**kwargs).all()

    @classmethod
    def search_by_value(cls, value):
        return cls.query.filter(cls.value.like('%' + value + '%')).all()

    @classmethod
    def generate_token(cls, owner_id):
        """Generates new token for a specified user and revokes all other
        tokens owned by this user.

        Returns:
            Value of the new token.
        """
        if owner_id is not None:
            last_hour_q = cls.query.filter(
                cls.owner_id == owner_id,
                cls.created > datetime.utcnow() - timedelta(hours=1),
            )
            if last_hour_q.count() > 0:
                raise TokenGenerationLimitException(
                    "Can't generate more than one token per hour.")
            cls.revoke_tokens(owner_id)

        new_token = cls(
            value=generate_string(TOKEN_LENGTH),
            owner_id=owner_id,
        )
        db.session.add(new_token)
        db.session.commit()

        TokenLog.create_record(new_token.value, token_log.ACTION_CREATE)

        return new_token.value

    @classmethod
    def revoke_tokens(cls, owner_id):
        """Revokes all tokens owned by a specified user.

        Args:
            owner_id: ID of a user.
        """
        tokens = db.session.query(cls).filter(cls.owner_id == owner_id,
                                              cls.is_active == True)
        for token in tokens:
            token.revoke()

    @classmethod
    def is_valid(cls, token_value):
        """Checks if token exists and is active."""
        token = cls.get(value=token_value)
        return token and token.is_active

    def revoke(self):
        self.is_active = False
        db.session.commit()
        TokenLog.create_record(self.value, token_log.ACTION_DEACTIVATE)