Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict analytics for unpublish engagement #2236

Merged
merged 15 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading