예제 #1
0
def source_create_access_logic(cls, user_or_token):
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)
    query = DBSession().query(cls)
    if not user_or_token.is_system_admin:
        query = query.join(Group).join(GroupUser)
        query = query.filter(GroupUser.user_id == user_id, GroupUser.can_save.is_(True))
    return query
예제 #2
0
def delete_obj_if_all_data_owned(cls, user_or_token):
    from .source import Source

    allow_nonadmins = cfg["misc.allow_nonadmins_delete_objs"] or False

    deletable_photometry = Photometry.query_records_accessible_by(
        user_or_token, mode="delete").subquery()
    nondeletable_photometry = (DBSession().query(Photometry.obj_id).join(
        deletable_photometry,
        deletable_photometry.c.id == Photometry.id,
        isouter=True,
    ).filter(deletable_photometry.c.id.is_(None)).distinct(
        Photometry.obj_id).subquery())

    deletable_spectra = Spectrum.query_records_accessible_by(
        user_or_token, mode="delete").subquery()
    nondeletable_spectra = (DBSession().query(Spectrum.obj_id).join(
        deletable_spectra,
        deletable_spectra.c.id == Spectrum.id,
        isouter=True,
    ).filter(deletable_spectra.c.id.is_(None)).distinct(
        Spectrum.obj_id).subquery())

    deletable_candidates = Candidate.query_records_accessible_by(
        user_or_token, mode="delete").subquery()
    nondeletable_candidates = (DBSession().query(Candidate.obj_id).join(
        deletable_candidates,
        deletable_candidates.c.id == Candidate.id,
        isouter=True,
    ).filter(deletable_candidates.c.id.is_(None)).distinct(
        Candidate.obj_id).subquery())

    deletable_sources = Source.query_records_accessible_by(
        user_or_token, mode="delete").subquery()
    nondeletable_sources = (DBSession().query(Source.obj_id).join(
        deletable_sources,
        deletable_sources.c.id == Source.id,
        isouter=True,
    ).filter(deletable_sources.c.id.is_(None)).distinct(
        Source.obj_id).subquery())

    return (DBSession().query(cls).join(
        nondeletable_photometry,
        nondeletable_photometry.c.obj_id == cls.id,
        isouter=True,
    ).filter(nondeletable_photometry.c.obj_id.is_(None)).join(
        nondeletable_spectra,
        nondeletable_spectra.c.obj_id == cls.id,
        isouter=True,
    ).filter(nondeletable_spectra.c.obj_id.is_(None)).join(
        nondeletable_candidates,
        nondeletable_candidates.c.obj_id == cls.id,
        isouter=True,
    ).filter(nondeletable_candidates.c.obj_id.is_(None)).join(
        nondeletable_sources,
        nondeletable_sources.c.obj_id == cls.id,
        isouter=True,
    ).filter(nondeletable_sources.c.obj_id.is_(None)).filter(
        sa.literal(allow_nonadmins)))
예제 #3
0
def groupuser_update_access_logic(cls, user_or_token):
    aliased = sa.orm.aliased(cls)
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)
    query = DBSession().query(cls).join(aliased,
                                        cls.group_id == aliased.group_id)
    if not user_or_token.is_system_admin:
        query = query.filter(aliased.user_id == user_id,
                             aliased.admin.is_(True))
    return query
예제 #4
0
 def add_linked_thumbnails(self):
     sdss_thumb = Thumbnail(photometry=self.photometry[0],
                            public_url=self.sdss_url,
                            type='sdss')
     dr8_thumb = Thumbnail(photometry=self.photometry[0],
                           public_url=self.desi_dr8_url,
                           type='dr8')
     DBSession().add_all([sdss_thumb, dr8_thumb])
     DBSession().commit()
예제 #5
0
파일: models.py 프로젝트: bnaul/skyportal
 def add_linked_thumbnails(self):
     sdss_thumb = Thumbnail(photometry=self.photometry[0],
                            public_url=self.get_sdss_url(),
                            type='sdss')
     ps1_thumb = Thumbnail(photometry=self.photometry[0],
                           public_url=self.get_panstarrs_url(),
                           type='ps1')
     DBSession().add_all([sdss_thumb, ps1_thumb])
     DBSession().commit()
예제 #6
0
def delete_single_user_group(mapper, connection, target):
    single_user_group = target.single_user_group

    # Delete single-user group
    @event.listens_for(DBSession(), "after_flush_postexec", once=True)
    def receive_after_flush(session, context):
        DBSession().delete(single_user_group)
예제 #7
0
    def query_accessible_rows(self, cls, user_or_token, columns=None):
        """Construct a Query object that, when executed, returns the rows of a
        specified table that are accessible to a specified user or token.

        Parameters
        ----------
        cls: `baselayer.app.models.DeclarativeMeta`
            The mapped class of the target table.
        user_or_token: `baselayer.app.models.User` or `baselayer.app.models.Token`
            The User or Token to check.
        columns: list of sqlalchemy.Column, optional, default None
            The columns to retrieve from the target table. If None, queries
            the mapped class directly and returns mapped instances.

        Returns
        -------
        query: sqlalchemy.Query
            Query for the accessible rows.
        """

        query = super().query_accessible_rows(cls,
                                              user_or_token,
                                              columns=columns)
        if not user_or_token.is_admin:
            # this avoids name collisions
            group_user_subq = (DBSession().query(GroupUser).filter(
                GroupUser.admin.is_(True)).subquery())
            query = query.join(
                group_user_subq,
                sa.and_(
                    Group.id == group_user_subq.c.group_id,
                    User.id == group_user_subq.c.user_id,
                ),
            )
        return query
예제 #8
0
 def wrapper(self, *args, **kwargs):
     token_header = self.request.headers.get("Authorization", None)
     if token_header is not None and token_header.startswith("token "):
         token_id = token_header.replace("token", "").strip()
         with DBSession() as session:
             token = session.scalars(
                 sa.select(Token).options(
                     joinedload(Token.created_by).options(
                         joinedload(User.acls),
                         joinedload(User.roles),
                     )).where(Token.id == token_id)).first()
         if token is not None:
             self.current_user = token
             if not token.created_by.is_active():
                 raise tornado.web.HTTPError(403, "User account expired")
         else:
             raise tornado.web.HTTPError(401)
         return method(self, *args, **kwargs)
     else:
         if self.current_user is not None:
             if not self.current_user.is_active():
                 raise tornado.web.HTTPError(403, "User account expired")
         else:
             raise tornado.web.HTTPError(
                 401,
                 'Credentials malformed; expected form "Authorization: token abc123"',
             )
         return tornado.web.authenticated(method)(self, *args, **kwargs)
예제 #9
0
def add_user_notifications(mapper, connection, target):
    # Add front-end user notifications
    @event.listens_for(DBSession(), "after_flush", once=True)
    def receive_after_flush(session, context):
        listing_subquery = (Listing.query.filter(
            Listing.list_name == "favorites").filter(
                Listing.obj_id == target.obj_id).distinct(
                    Listing.user_id).subquery())
        users = (User.query.join(
            listing_subquery, User.id == listing_subquery.c.user_id).filter(
                User.preferences["favorite_sources_activity_notifications"][
                    target.__tablename__].astext.cast(
                        sa.Boolean).is_(True)).all())
        ws_flow = Flow()
        for user in users:
            # Only notify users who have read access to the new record in question
            if target.__class__.get_if_accessible_by(target.id,
                                                     user) is not None:
                session.add(
                    UserNotification(
                        user=user,
                        text=
                        f"New {target.__class__.__name__.lower()} on your favorite source *{target.obj_id}*",
                        url=f"/source/{target.obj_id}",
                    ))
                ws_flow.push(user.id, "skyportal/FETCH_NOTIFICATIONS")
예제 #10
0
def manage_shift_access_logic(cls, user_or_token):
    # admins of the shift and admins of the group associated with the shift can delete and update a shift
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)
    query = DBSession().query(cls).join(GroupUser, cls.group_id == GroupUser.group_id)
    if not user_or_token.is_system_admin:
        admin_query = query.filter(
            GroupUser.user_id == user_id, GroupUser.admin.is_(True)
        )
        if admin_query.count() == 0:
            query = query.join(ShiftUser)
            query = query.filter(
                ShiftUser.user_id == user_id, ShiftUser.admin.is_(True)
            )
        else:
            query = admin_query
    return query
예제 #11
0
def update_single_user_group(mapper, connection, target):

    # Update single user group name if needed
    @event.listens_for(DBSession(), "after_flush_postexec", once=True)
    def receive_after_flush(session, context):
        single_user_group = target.single_user_group
        single_user_group.name = slugify(target.username)
        DBSession().merge(single_user_group)
예제 #12
0
def create_single_user_group(mapper, connection, target):

    # Create single-user group
    @event.listens_for(DBSession(), "after_flush", once=True)
    def receive_after_flush(session, context):
        session.add(
            Group(name=slugify(target.username),
                  users=[target],
                  single_user_group=True))
예제 #13
0
def shiftuser_update_access_logic(cls, user_or_token):
    aliased = safe_aliased(cls)
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)
    user_shift_admin = (
        DBSession()
        .query(Shift)
        .join(GroupUser, GroupUser.group_id == Shift.group_id)
        .filter(sa.and_(GroupUser.user_id == user_id, GroupUser.admin.is_(True)))
    )
    query = DBSession().query(cls).join(aliased, cls.shift_id == aliased.shift_id)
    if not user_or_token.is_system_admin:
        query = query.filter(
            sa.or_(
                aliased.user_id == user_id,
                sa.and_(aliased.admin.is_(True), aliased.user_id == user_id),
                aliased.shift_id.in_([shift.id for shift in user_shift_admin.all()]),
            )
        )
    return query
예제 #14
0
    def probability(self, cumulative_probability=1.0):
        """Integrated probability within a given localization."""

        cum_prob = (sa.func.sum(
            LocalizationTile.probdensity * LocalizationTile.healpix.area).over(
                order_by=LocalizationTile.probdensity.desc()).label('cum_prob')
                    )
        localizationtile_subquery = (sa.select(
            LocalizationTile.probdensity, cum_prob).where(
                LocalizationTile.localization_id ==
                self.observation_plan_request.localization_id, ).distinct()
                                     ).subquery()

        min_probdensity = (sa.select(
            sa.func.min(localizationtile_subquery.columns.probdensity)).where(
                localizationtile_subquery.columns.cum_prob <=
                cumulative_probability)).scalar_subquery()

        tiles_subquery = (sa.select(InstrumentFieldTile.id).where(
            LocalizationTile.localization_id ==
            self.observation_plan_request.localization_id,
            LocalizationTile.probdensity >= min_probdensity,
            InstrumentFieldTile.instrument_id == self.instrument_id,
            InstrumentFieldTile.instrument_field_id == InstrumentField.id,
            InstrumentFieldTile.instrument_field_id ==
            PlannedObservation.field_id,
            PlannedObservation.observation_plan_id == self.id,
            InstrumentFieldTile.healpix.overlaps(LocalizationTile.healpix),
        ).distinct().subquery())

        tiles_subquery = (sa.select(InstrumentFieldTile.id).where(
            LocalizationTile.localization_id ==
            self.observation_plan_request.localization_id,
            LocalizationTile.probdensity >= min_probdensity,
            InstrumentFieldTile.instrument_id == self.instrument_id,
            InstrumentFieldTile.instrument_field_id == InstrumentField.id,
            InstrumentFieldTile.instrument_field_id ==
            PlannedObservation.field_id,
            PlannedObservation.observation_plan_id == self.id,
            InstrumentFieldTile.healpix.overlaps(LocalizationTile.healpix),
        ).distinct().subquery())

        union = sa.select(
            ha.func.union(InstrumentFieldTile.healpix).label('healpix'))
        union = union.join(tiles_subquery,
                           tiles_subquery.c.id == InstrumentFieldTile.id)
        prob = sa.func.sum(
            LocalizationTile.probdensity *
            (union.columns.healpix * LocalizationTile.healpix).area)
        query_prob = sa.select(prob)
        intprob = DBSession().execute(query_prob).scalar_one()
        if intprob is None:
            intprob = 0.0

        return intprob
예제 #15
0
def user_update_delete_logic(cls, user_or_token):
    """A user can update or delete themselves, and a super admin can delete
    or update any user."""

    if user_or_token.is_admin:
        return public.query_accessible_rows(cls, user_or_token)

    # non admin users can only update or delete themselves
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)

    return DBSession().query(cls).filter(cls.id == user_id)
예제 #16
0
def delete_group_access_logic(cls, user_or_token):
    """User can delete a group that is not the sitewide public group, is not
    a single user group, and that they are an admin member of."""
    user_id = UserAccessControl.user_id_from_user_or_token(user_or_token)
    query = (DBSession().query(cls).join(GroupUser).filter(
        cls.name != cfg['misc']['public_group_name']).filter(
            cls.single_user_group.is_(False)))
    if not user_or_token.is_system_admin:
        query = query.filter(GroupUser.user_id == user_id,
                             GroupUser.admin.is_(True))
    return query
예제 #17
0
def get_candidate_if_owned_by(obj_id, user_or_token, options=[]):
    if Candidate.query.filter(Candidate.obj_id == obj_id).first() is None:
        return None
    user_group_ids = [g.id for g in user_or_token.groups]
    c = (Candidate.query.filter(Candidate.obj_id == obj_id).filter(
        Candidate.filter_id.in_(
            DBSession.query(Filter.id).filter(
                Filter.group_id.in_(user_group_ids)))).options(
                    options).first())
    if c is None:
        raise AccessError("Insufficient permissions.")
    return c.obj
예제 #18
0
    def query_accessible_rows(self, cls, user_or_token, columns=None):
        """Construct a Query object that, when executed, returns the rows of a
        specified table that are accessible to a specified user or token.

        Parameters
        ----------
        cls: `baselayer.app.models.DeclarativeMeta`
            The mapped class of the target table.
        user_or_token: `baselayer.app.models.User` or `baselayer.app.models.Token`
            The User or Token to check.
        columns: list of sqlalchemy.Column, optional, default None
            The columns to retrieve from the target table. If None, queries
            the mapped class directly and returns mapped instances.

        Returns
        -------
        query: sqlalchemy.Query
            Query for the accessible rows.
        """

        # system admins automatically get full access
        if user_or_token.is_admin:
            return public.query_accessible_rows(cls,
                                                user_or_token,
                                                columns=columns)

        # return only selected columns if requested
        if columns is not None:
            query = DBSession().query(*columns).select_from(cls)
        else:
            query = DBSession().query(cls)

        # traverse the relationship chain via sequential JOINs
        for relationship_name in self.relationship_names:
            self.check_cls_for_attributes(cls, [relationship_name])
            relationship = sa.inspect(
                cls).mapper.relationships[relationship_name]
            # not a private attribute, just has an underscore to avoid name
            # collision with python keyword
            cls = relationship.entity.class_

            if str(relationship) == "Group.users":
                # For the last relationship between Group and User, just join
                # in the join table and not the join table and the full User table
                # since we only need the GroupUser.user_id field to match on
                query = query.join(GroupUser)
            else:
                query = query.join(relationship.class_attribute)

        # filter for records with at least one matching user
        user_id = self.user_id_from_user_or_token(user_or_token)
        query = query.filter(GroupUser.user_id == user_id)
        return query
예제 #19
0
def taxonomy_update_delete_logic(cls, user_or_token):
    """This function generates the query for taxonomies that the current user
    can update or delete. If the querying user doesn't have System admin or
    Delete taxonomy acl, then no taxonomies are accessible to that user under
    this policy . Otherwise, the only taxonomies that the user can delete are
    those that have no associated classifications, preventing classifications
    from getting deleted in a cascade when their parent taxonomy is deleted.
    """
    from .classification import Classification

    if len({'Delete taxonomy', 'System admin'}
           & set(user_or_token.permissions)) == 0:
        # nothing accessible
        return restricted.query_accessible_rows(cls, user_or_token)

    # dont allow deletion of any taxonomies that have classifications attached
    return (DBSession().query(cls).outerjoin(Classification).group_by(
        cls.id).having(sa.func.bool_and(Classification.id.is_(None))))
예제 #20
0
def insert_into_phot_stat(mapper, connection, target):

    # Create or update PhotStat object
    @event.listens_for(DBSession(), "before_flush", once=True)
    def receive_after_flush(session, context, instances):
        obj_id = target.obj_id
        phot_stat = session.scalars(
            sa.select(PhotStat).where(PhotStat.obj_id == obj_id)).first()
        if phot_stat is None:
            all_phot = session.scalars(
                sa.select(Photometry).where(
                    Photometry.obj_id == obj_id)).all()
            phot_stat = PhotStat(obj_id=obj_id)
            phot_stat.full_update(all_phot)
            session.add(phot_stat)

        else:
            phot_stat.add_photometry_point(target)
            session.add(phot_stat)
예제 #21
0
def updatable_by_token_with_listener_acl(cls, user_or_token):
    if user_or_token.is_admin:
        return public.query_accessible_rows(cls, user_or_token)

    instruments_with_apis = (
        Instrument.query_records_accessible_by(user_or_token).filter(
            Instrument.listener_classname.isnot(None)).all())

    api_map = {
        instrument.id: instrument.listener_class.get_acl_id()
        for instrument in instruments_with_apis
    }

    accessible_instrument_ids = [
        instrument_id for instrument_id, acl_id in api_map.items()
        if acl_id in user_or_token.permissions
    ]

    return (DBSession().query(cls).join(Allocation).join(Instrument).filter(
        Instrument.id.in_(accessible_instrument_ids)))
예제 #22
0
def stream_delete_logic(cls, user_or_token):
    """Can only delete a stream from a user if none of the user's groups
    require that stream for membership.
    """
    from .group_joins import GroupStream

    return (
        DBSession().query(cls).filter(
            sa.literal(
                user_or_token.is_admin)).join(
                    User, cls.user).outerjoin(
                        Group, User.groups).outerjoin(
                            GroupStream,
                            sa.and_(
                                GroupStream.group_id == Group.id,
                                GroupStream.stream_id == cls.stream_id,
                            ),
                        ).group_by(cls.id)
        # no OR here because Users will always be a member of at least one
        # group -- their single user group.
        .having(sa.func.bool_and(GroupStream.stream_id.is_(None))))
예제 #23
0
def group_create_logic(cls, user_or_token):
    """
    Can only add a user to a group if they have all the requisite
    streams required for entry to the group. And users cannot
    be added to single user groups through the Groups API (only
    through event handlers).
    """
    from .stream import Stream, StreamUser

    return (DBSession().query(cls).join(Group).outerjoin(
        Stream, Group.streams).outerjoin(
            StreamUser,
            sa.and_(
                StreamUser.user_id == cls.user_id,
                StreamUser.stream_id == Stream.id,
            ),
        ).filter(Group.single_user_group.is_(False)).group_by(cls.id).having(
            sa.or_(
                sa.func.bool_and(StreamUser.stream_id.isnot(None)),
                sa.func.bool_and(Stream.id.is_(None)),  # group has no streams
            )))
예제 #24
0
    def area(self):
        """Integrated area in sq. deg within localization."""

        union = (sa.select(
            ha.func.union(
                InstrumentFieldTile.healpix).label('healpix')).filter(
                    InstrumentFieldTile.instrument_field_id ==
                    PlannedObservation.field_id,
                    PlannedObservation.observation_plan_id == self.id,
                ).subquery())

        area = sa.func.sum(union.columns.healpix.area)
        query_area = sa.select(area).filter(
            LocalizationTile.localization_id ==
            self.observation_plan_request.localization_id,
            union.columns.healpix.overlaps(LocalizationTile.healpix),
        )
        intarea = DBSession().execute(query_area).scalar_one()

        if intarea is None:
            intarea = 0.0
        return intarea * (180.0 / np.pi)**2
예제 #25
0
    def probability(self):
        """Integrated probability within a given localization."""

        union = (sa.select(
            ha.func.union(
                InstrumentFieldTile.healpix).label('healpix')).filter(
                    InstrumentFieldTile.instrument_field_id ==
                    PlannedObservation.field_id,
                    PlannedObservation.observation_plan_id == self.id,
                ).subquery())

        prob = sa.func.sum(
            LocalizationTile.probdensity *
            (union.columns.healpix * LocalizationTile.healpix).area)
        query_prob = sa.select(prob).filter(
            LocalizationTile.localization_id ==
            self.observation_plan_request.localization_id,
            union.columns.healpix.overlaps(LocalizationTile.healpix),
        )
        intprob = DBSession().execute(query_prob).scalar_one()
        if intprob is None:
            intprob = 0.0

        return intprob
예제 #26
0
def send_source_notification(mapper, connection, target):
    app_base_url = get_app_base_url()

    link_location = f'{app_base_url}/source/{target.source_id}'
    if target.sent_by.first_name is not None and target.sent_by.last_name is not None:
        sent_by_name = f'{target.sent_by.first_name} {target.sent_by.last_name}'
    else:
        sent_by_name = target.sent_by.username

    group_ids = map(lambda group: group.id, target.groups)
    groups = DBSession().query(Group).filter(Group.id.in_(group_ids)).all()

    target_users = set()
    for group in groups:
        # Use a set to get unique iterable of users
        target_users.update(group.users)

    source = DBSession().query(Obj).get(target.source_id)
    source_info = ""
    if source.ra is not None:
        source_info += f'RA={source.ra} '
    if source.dec is not None:
        source_info += f'Dec={source.dec}'
    source_info = source_info.strip()

    # Send SMS messages to opted-in users if desired
    if target.level == "hard":
        message_text = (
            f'{cfg["app.title"]}: {sent_by_name} would like to call your immediate'
            f' attention to a source at {link_location} ({source_info}).')
        if target.additional_notes != "" and target.additional_notes is not None:
            message_text += f' Addtional notes: {target.additional_notes}'

        account_sid = cfg["twilio.sms_account_sid"]
        auth_token = cfg["twilio.sms_auth_token"]
        from_number = cfg["twilio.from_number"]
        client = TwilioClient(account_sid, auth_token)
        for user in target_users:
            # If user has a phone number registered and opted into SMS notifications
            if (user.contact_phone is not None and user.preferences is not None
                    and "allowSMSAlerts" in user.preferences
                    and user.preferences.get("allowSMSAlerts")):
                client.messages.create(body=message_text,
                                       from_=from_number,
                                       to=user.contact_phone.e164)

    # Send email notifications
    recipients = []
    for user in target_users:
        # If user has a contact email registered and opted into email notifications
        if (user.contact_email is not None and user.preferences is not None
                and "allowEmailAlerts" in user.preferences
                and user.preferences.get("allowEmailAlerts")):
            recipients.append(user.contact_email)

    descriptor = "immediate" if target.level == "hard" else ""
    html_content = (
        f'{sent_by_name} would like to call your {descriptor} attention to'
        f' <a href="{link_location}">{target.source_id}</a> ({source_info})')
    if target.additional_notes != "" and target.additional_notes is not None:
        html_content += f'<br /><br />Additional notes: {target.additional_notes}'

    if len(recipients) > 0:
        send_email(
            recipients=recipients,
            subject=f'{cfg["app.title"]}: Source Alert',
            body=html_content,
        )
예제 #27
0
def get_users(role=None):
    return [user[0] for user in DBSession().execute(sa.select(User))]
예제 #28
0
 def receive_after_flush(session, context):
     single_user_group = target.single_user_group
     single_user_group.name = slugify(target.username)
     DBSession().merge(single_user_group)
예제 #29
0
 def receive_after_flush(session, context):
     DBSession().delete(single_user_group)
예제 #30
0
@property
def user_or_token_accessible_streams(self):
    """Return the list of Streams a User or Token has access to."""
    if "System admin" in self.permissions:
        return Stream.query.all()
    if isinstance(self, Token):
        return self.created_by.streams
    return self.streams


User.to_dict = user_to_dict
User.accessible_groups = user_or_token_accessible_groups
User.accessible_streams = user_or_token_accessible_streams
User.single_user_group = property(lambda self: DBSession().query(Group).join(
    GroupUser).filter(Group.single_user_group.is_(True), GroupUser.user_id ==
                      self.id).first())
User.streams = relationship(
    'Stream',
    secondary='stream_users',
    back_populates='users',
    passive_deletes=True,
    doc="The Streams this User has access to.",
)
User.groups = relationship(
    'Group',
    secondary='group_users',
    back_populates='users',
    passive_deletes=True,
    doc="The Groups this User is a member of.",
)