Skip to content

Commit

Permalink
Restrict analytics for unpublish engagement (#2236)
Browse files Browse the repository at this point in the history
* Changes to show all survey results to superusers

* removing hard coded values

* fixing linting

* splitting to seperate end points

* fixing auth check

* fixing linting

* merging method in service

* Handle no data error for graphs

* adding new nodata component

* adding new email for submission response

* fixing linting and testing

* Upgrades to Issue Tracking Table

* removing try catch

* Updated dagster user code deployment name

* Restrict analytics for unpublish engagement
  • Loading branch information
VineetBala-AOT authored Sep 21, 2023
1 parent 6240bfe commit 8cba3d0
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/met-etl-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ defaults:

env:
APP_NAME: "dagster-etl"
DEPLOYMENT_NAME: "dagster-dagster-user-deployments-k8s-example-user-code-1"
DEPLOYMENT_NAME: "dagster-dagster-user-deployments-etl"
TAG_NAME: "dev"

PROJECT_TYPE: "${{ github.event.inputs.project_type || 'EAO' }}" # If the project type is manually selected, use the input value; otherwise, use 'EAO' as default
Expand Down
25 changes: 25 additions & 0 deletions analytics-api/src/analytics_api/constants/engagement_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright © 2021 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants of engagement status."""
from enum import Enum


class Status(Enum):
"""Enum of engagement status."""

Draft = 'Draft'
Published = 'Published'
Closed = 'Closed'
Scheduled = 'Scheduled'
Unpublished = 'Unpublished'
14 changes: 7 additions & 7 deletions analytics-api/src/analytics_api/services/aggregator_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Service to get counts for dashboard."""
from analytics_api.models.email_verification import EmailVerification as EmailVerificationModel
from analytics_api.models.user_response_detail import UserResponseDetail as UserResponseDetailModel
from analytics_api.utils import engagement_access_validator


class AggregatorService: # pylint: disable=too-few-public-methods
Expand All @@ -11,11 +12,10 @@ class AggregatorService: # pylint: disable=too-few-public-methods
@staticmethod
def get_count(engagement_id, count_for=''):
"""Get total count for an engagement id."""
total_count = 0
if engagement_access_validator.check_engagement_access(engagement_id):
if count_for == 'email_verification':
return EmailVerificationModel.get_email_verification_count(engagement_id)
if count_for == 'survey_completed':
return UserResponseDetailModel.get_response_count(engagement_id)

if count_for == 'email_verification':
total_count = EmailVerificationModel.get_email_verification_count(engagement_id)
if count_for == 'survey_completed':
total_count = UserResponseDetailModel.get_response_count(engagement_id)

return total_count
return 0
17 changes: 11 additions & 6 deletions analytics-api/src/analytics_api/services/engagement_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from analytics_api.models.engagement import Engagement as EngagementModel
from analytics_api.schemas.engagement import EngagementSchema
from analytics_api.schemas.map_data import MapDataSchema
from analytics_api.utils import engagement_access_validator


class EngagementService: # pylint: disable=too-few-public-methods
Expand All @@ -12,13 +13,17 @@ class EngagementService: # pylint: disable=too-few-public-methods
@staticmethod
def get_engagement(engagement_id) -> EngagementSchema:
"""Get Engagement by the id."""
engagement = EngagementModel.find_by_id(engagement_id)
engagement_schema = EngagementSchema()
return engagement_schema.dump(engagement)
if engagement_access_validator.check_engagement_access(engagement_id):
engagement = EngagementModel.find_by_id(engagement_id)
engagement_schema = EngagementSchema()
return engagement_schema.dump(engagement)
return {}

@staticmethod
def get_engagement_map_data(engagement_id) -> MapDataSchema:
"""Get Map data by the engagement id."""
map_data = EngagementModel.get_engagement_map_data(engagement_id)
map_data_schema = MapDataSchema()
return map_data_schema.dump(map_data)
if engagement_access_validator.check_engagement_access(engagement_id):
map_data = EngagementModel.get_engagement_map_data(engagement_id)
map_data_schema = MapDataSchema()
return map_data_schema.dump(map_data)
return {}
9 changes: 6 additions & 3 deletions analytics-api/src/analytics_api/services/survey_result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Service for survey result management."""
from analytics_api.models.request_type_option import RequestTypeOption as RequestTypeOptionModel
from analytics_api.schemas.survey_result import SurveyResultSchema
from analytics_api.utils import engagement_access_validator


class SurveyResultService: # pylint: disable=too-few-public-methods
Expand All @@ -11,6 +12,8 @@ class SurveyResultService: # pylint: disable=too-few-public-methods
@staticmethod
def get_survey_result(engagement_id, can_view_all_survey_results) -> SurveyResultSchema:
"""Get Survey result by the engagement id."""
survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results)
survey_result_schema = SurveyResultSchema(many=True)
return survey_result_schema.dump(survey_result)
if engagement_access_validator.check_engagement_access(engagement_id):
survey_result = RequestTypeOptionModel.get_survey_result(engagement_id, can_view_all_survey_results)
survey_result_schema = SurveyResultSchema(many=True)
return survey_result_schema.dump(survey_result)
return {}
17 changes: 11 additions & 6 deletions analytics-api/src/analytics_api/services/user_response_detail.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Service for user response detail management."""
from analytics_api.models.user_response_detail import UserResponseDetail as UserResponseDetailModel
from analytics_api.utils import engagement_access_validator


class UserResponseDetailService:
Expand All @@ -10,13 +11,17 @@ class UserResponseDetailService:
@staticmethod
def get_response_count_by_created_month(engagement_id, search_options=None):
"""Get user response count for an engagement id grouped by created month."""
response_count_by_created_month = UserResponseDetailModel.get_response_count_by_created_month(
engagement_id, search_options)
return response_count_by_created_month
if engagement_access_validator.check_engagement_access(engagement_id):
response_count_by_created_month = UserResponseDetailModel.get_response_count_by_created_month(
engagement_id, search_options)
return response_count_by_created_month
return {}

@staticmethod
def get_response_count_by_created_week(engagement_id, search_options=None):
"""Get user response count for an engagement id grouped by created week."""
response_count_by_created_week = UserResponseDetailModel.get_response_count_by_created_week(
engagement_id, search_options)
return response_count_by_created_week
if engagement_access_validator.check_engagement_access(engagement_id):
response_count_by_created_week = UserResponseDetailModel.get_response_count_by_created_week(
engagement_id, search_options)
return response_count_by_created_week
return {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Check Engagement Access Service."""
from sqlalchemy import and_, exists
from sqlalchemy.sql.expression import true
from analytics_api.constants.engagement_status import Status
from analytics_api.models.db import db
from analytics_api.models.engagement import Engagement as EngagementModel
from analytics_api.utils.roles import Role
from analytics_api.utils.token_info import TokenInfo


def check_engagement_access(engagement_id):
"""Check if user has access to get engagement details."""
is_engagement_unpublished = db.session.query(
exists()
.where(
and_(
EngagementModel.source_engagement_id == engagement_id,
EngagementModel.is_active == true(),
EngagementModel.status_name == Status.Unpublished.value
)
)
).scalar()

user_roles = set(TokenInfo.get_user_roles())

return not is_engagement_unpublished or Role.ACCESS_DASHBOARD.value in user_roles
1 change: 1 addition & 0 deletions analytics-api/src/analytics_api/utils/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ class Role(Enum):
"""User Role."""

VIEW_ALL_SURVEY_RESULTS = 'view_all_survey_results' # Allows users to view results to all questions
ACCESS_DASHBOARD = 'access_dashboard'
43 changes: 43 additions & 0 deletions analytics-api/src/analytics_api/utils/token_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Helper for token decoding."""
from flask import current_app, g

from analytics_api.utils.roles import Role
from analytics_api.utils.user_context import UserContext, user_context


class TokenInfo:
"""Token info."""

@staticmethod
@user_context
def get_id(**kwargs):
"""Get the user identifier."""
try:
user_from_context: UserContext = kwargs['user_context']
return user_from_context.sub
except AttributeError:
return None

@staticmethod
def get_user_data():
"""Get the user data."""
token_info = g.jwt_oidc_token_info
user_data = {
'external_id': token_info.get('sub', None),
'first_name': token_info.get('given_name', None),
'last_name': token_info.get('family_name', None),
'email_address': token_info.get('email', None),
'username': token_info.get('preferred_username', None),
'identity_provider': token_info.get('identity_provider', ''),
'roles': TokenInfo.get_user_roles(),
}
return user_data

@staticmethod
def get_user_roles():
"""Get the user roles from token."""
if not hasattr(g, 'jwt_oidc_token_info') or not g.jwt_oidc_token_info:
return []
valid_roles = set(item.value for item in Role)
token_roles = current_app.config['JWT_ROLE_CALLBACK'](g.jwt_oidc_token_info)
return valid_roles.intersection(token_roles)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dagster import Out, Output, op
from met_api.constants.engagement_status import Status as MetEngagementStatus
from met_api.models.engagement import Engagement as MetEngagementModel
from met_api.models.engagement_status import EngagementStatus as EngagementStatusModel
from met_api.models.widget_map import WidgetMap as MetWidgetMap
from analytics_api.models.engagement import Engagement as EtlEngagementModel
from sqlalchemy import func
Expand Down Expand Up @@ -102,6 +103,9 @@ def load_engagement(context, new_engagements, updated_engagements, engagement_ne
geojson=map_widget.geojson
marker_label=map_widget.marker_label

engagement_status = met_session.query(EngagementStatusModel).filter(
EngagementStatusModel.id == engagement.status_id).first()

engagement_model = EtlEngagementModel(name=engagement.name,
source_engagement_id=engagement.id,
start_date=engagement.start_date,
Expand All @@ -115,6 +119,7 @@ def load_engagement(context, new_engagements, updated_engagements, engagement_ne
longitude=longitude,
geojson=geojson,
marker_label=marker_label,
status_name=engagement_status.status_name
)
session.add(engagement_model)
session.commit()
Expand Down

0 comments on commit 8cba3d0

Please sign in to comment.