diff --git a/README.md b/README.md index ade8dee601..94bea92f86 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,10 @@ More details in the [FCM documentation](https://firebase.google.com/docs/admin/s ##### Quicksight Dashboards For generating quicksight dashboard links the following environment variable is required -`QUICKSIGHT_AUTHORIZED_ARNS` - A comma separated list of `arn:aws:quicksight:...` format ARN strings +`QUICKSIGHT_AUTHORIZED_ARNS` - A dictionary of the format `"": ["arn:aws:quicksight:...",...]` +where each quicksight dashboard gets treated with an arbitrary "name", and a list of "authorized arns". +The first the "authorized arns" is always considered as the `InitialDashboardID` when creating an embed URL +for the respective "dashboard name". ### Email sending diff --git a/api/admin/controller/quicksight.py b/api/admin/controller/quicksight.py index 4ebb9d5f9a..2e36d23523 100644 --- a/api/admin/controller/quicksight.py +++ b/api/admin/controller/quicksight.py @@ -5,6 +5,7 @@ import flask from api.admin.model.quicksight import ( + QuicksightDashboardNamesResponse, QuicksightGenerateUrlRequest, QuicksightGenerateUrlResponse, ) @@ -18,13 +19,13 @@ class QuickSightController(CirculationManagerController): - def generate_quicksight_url(self, dashboard_id) -> Dict: + def generate_quicksight_url(self, dashboard_name) -> Dict: log = logging.getLogger(self.__class__.__name__) admin: Admin = getattr(flask.request, "admin") request_data = QuicksightGenerateUrlRequest(**flask.request.args) - authorized_arns = Configuration.quicksight_authorized_arns() - if not authorized_arns: + all_authorized_arns = Configuration.quicksight_authorized_arns() + if not all_authorized_arns: log.error("No Quicksight ARNs were configured for this server.") raise ProblemError( INTERNAL_SERVER_ERROR.detailed( @@ -32,21 +33,23 @@ def generate_quicksight_url(self, dashboard_id) -> Dict: ) ) - for arn in authorized_arns: - # format aws:arn:quicksight::: - arn_parts = arn.split(":") - if f"dashboard/{dashboard_id}" == arn_parts[5]: - # Pull the region and account id from the ARN - aws_account_id = arn_parts[4] - region = arn_parts[3] - break - else: + authorized_arns = all_authorized_arns.get(dashboard_name) + if not authorized_arns: raise ProblemError( INVALID_INPUT.detailed( "The requested Dashboard ARN is not recognized by this server." ) ) + # The first dashboard id is the primary ARN + dashboard_arn = authorized_arns[0] + # format aws:arn:quicksight::: + arn_parts = dashboard_arn.split(":") + # Pull the region and account id from the ARN + aws_account_id = arn_parts[4] + region = arn_parts[3] + dashboard_id = arn_parts[5].split("/", 1)[1] # drop the "dashboard/" part + allowed_libraries = [] for library in self._db.query(Library).all(): if admin.is_librarian(library): @@ -107,3 +110,8 @@ def generate_quicksight_url(self, dashboard_id) -> Dict: ) return QuicksightGenerateUrlResponse(embed_url=embed_url).api_dict() + + def get_dashboard_names(self): + """Get the named dashboard IDs defined in the configuration""" + config = Configuration.quicksight_authorized_arns() + return QuicksightDashboardNamesResponse(names=list(config.keys())).api_dict() diff --git a/api/admin/model/quicksight.py b/api/admin/model/quicksight.py index de6533e317..752f889e37 100644 --- a/api/admin/model/quicksight.py +++ b/api/admin/model/quicksight.py @@ -17,3 +17,7 @@ def parse_library_ids(cls, value): class QuicksightGenerateUrlResponse(CustomBaseModel): embed_url: str = Field(description="The dashboard embed url.") + + +class QuicksightDashboardNamesResponse(CustomBaseModel): + names: List[str] = Field(description="The named quicksight dashboard ids") diff --git a/api/admin/routes.py b/api/admin/routes.py index 21c1ae05c8..c1b8806454 100644 --- a/api/admin/routes.py +++ b/api/admin/routes.py @@ -11,6 +11,7 @@ from api.admin.dashboard_stats import generate_statistics from api.admin.model.dashboard_statistics import StatisticsResponse from api.admin.model.quicksight import ( + QuicksightDashboardNamesResponse, QuicksightGenerateUrlRequest, QuicksightGenerateUrlResponse, ) @@ -348,7 +349,7 @@ def stats(): return statistics_response.api_dict() -@app.route("/admin/quicksight_embed/") +@app.route("/admin/quicksight_embed/") @api_spec.validate( resp=SpecResponse(HTTP_200=QuicksightGenerateUrlResponse), tags=["admin.quicksight"], @@ -356,8 +357,21 @@ def stats(): ) @returns_json_or_response_or_problem_detail @requires_admin -def generate_quicksight_url(dashboard_id: str): - return app.manager.admin_quicksight_controller.generate_quicksight_url(dashboard_id) +def generate_quicksight_url(dashboard_name: str): + return app.manager.admin_quicksight_controller.generate_quicksight_url( + dashboard_name + ) + + +@app.route("/admin/quicksight_embed/names") +@api_spec.validate( + resp=SpecResponse(HTTP_200=QuicksightDashboardNamesResponse), + tags=["admin.quicksight"], +) +@returns_json_or_response_or_problem_detail +@requires_admin +def get_quicksight_names(): + return app.manager.admin_quicksight_controller.get_dashboard_names() @app.route("/admin/libraries", methods=["GET", "POST"]) diff --git a/core/config.py b/core/config.py index 28b24bdc18..2bc742a46a 100644 --- a/core/config.py +++ b/core/config.py @@ -268,10 +268,10 @@ def overdrive_fulfillment_keys(cls, testing=False) -> Dict[str, str]: return {"key": key, "secret": secret} @classmethod - def quicksight_authorized_arns(cls) -> List[str]: + def quicksight_authorized_arns(cls) -> Dict[str, List[str]]: """Split the comma separated arns""" arns_str = os.environ.get(cls.QUICKSIGHT_AUTHORIZED_ARNS_KEY, "") - return arns_str.split(",") + return json.loads(arns_str) @classmethod def localization_languages(cls): diff --git a/tests/api/admin/controller/test_quicksight.py b/tests/api/admin/controller/test_quicksight.py index 2078d11a90..8f007c88b5 100644 --- a/tests/api/admin/controller/test_quicksight.py +++ b/tests/api/admin/controller/test_quicksight.py @@ -38,10 +38,16 @@ def test_generate_quicksight_url( ) as mock_boto, mock.patch( "api.admin.controller.quicksight.Configuration.quicksight_authorized_arns" ) as mock_qs_arns: - arns = [ - "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid1", - "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid2", - ] + arns = dict( + primary=[ + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid1", + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid2", + ], + secondary=[ + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid2", + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid1", + ], + ) mock_qs_arns.return_value = arns generate_method: mock.MagicMock = ( mock_boto.client().generate_embed_url_for_anonymous_user @@ -52,7 +58,7 @@ def test_generate_quicksight_url( f"/?library_ids={default.id},{library1.id},30000", admin=system_admin, ) as ctx: - response = ctrl.generate_quicksight_url("uuid1") + response = ctrl.generate_quicksight_url("primary") # Assert the right client was created, with a region assert mock_boto.client.call_args == mock.call( @@ -63,7 +69,7 @@ def test_generate_quicksight_url( assert generate_method.call_args == mock.call( AwsAccountId="aws-account-id", Namespace="default", - AuthorizedResourceArns=arns, + AuthorizedResourceArns=arns["primary"], ExperienceConfiguration={ "Dashboard": {"InitialDashboardId": "uuid1"} }, @@ -81,12 +87,12 @@ def test_generate_quicksight_url( admin=admin1, ) as ctx: generate_method.reset_mock() - ctrl.generate_quicksight_url("uuid2") + ctrl.generate_quicksight_url("secondary") assert generate_method.call_args == mock.call( AwsAccountId="aws-account-id", Namespace="default", - AuthorizedResourceArns=arns, + AuthorizedResourceArns=arns["secondary"], ExperienceConfiguration={ "Dashboard": {"InitialDashboardId": "uuid2"} }, @@ -111,10 +117,12 @@ def test_generate_quicksight_url_errors( ) as mock_boto, mock.patch( "api.admin.controller.quicksight.Configuration.quicksight_authorized_arns" ) as mock_qs_arns: - arns = [ - "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid1", - "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid2", - ] + arns = dict( + primary=[ + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid1", + "arn:aws:quicksight:us-west-1:aws-account-id:dashboard/uuid2", + ] + ) mock_qs_arns.return_value = arns with quicksight_fixture.request_context_with_admin( @@ -122,7 +130,7 @@ def test_generate_quicksight_url_errors( admin=admin, ) as ctx: with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid-none") + ctrl.generate_quicksight_url("secondary") assert ( raised.value.problem_detail.detail == "The requested Dashboard ARN is not recognized by this server." @@ -130,7 +138,7 @@ def test_generate_quicksight_url_errors( mock_qs_arns.return_value = [] with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid1") + ctrl.generate_quicksight_url("primary") assert ( raised.value.problem_detail.detail == "Quicksight has not been configured for this server." @@ -142,7 +150,7 @@ def test_generate_quicksight_url_errors( ) as ctx: mock_qs_arns.return_value = arns with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid1") + ctrl.generate_quicksight_url("primary") assert ( raised.value.problem_detail.detail == "No library was found for this Admin that matched the request." @@ -157,7 +165,7 @@ def test_generate_quicksight_url_errors( status=400, embed_url="http://embed" ) with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid1") + ctrl.generate_quicksight_url("primary") assert ( raised.value.problem_detail.detail == "Error while fetching the Quisksight Embed url." @@ -168,7 +176,7 @@ def test_generate_quicksight_url_errors( status=200, ) with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid1") + ctrl.generate_quicksight_url("primary") assert ( raised.value.problem_detail.detail == "Error while fetching the Quisksight Embed url." @@ -179,8 +187,18 @@ def test_generate_quicksight_url_errors( "" ) with pytest.raises(ProblemError) as raised: - ctrl.generate_quicksight_url("uuid1") + ctrl.generate_quicksight_url("primary") assert ( raised.value.problem_detail.detail == "Error while fetching the Quisksight Embed url." ) + + def test_get_dashboard_names(self, quicksight_fixture: QuickSightControllerFixture): + with mock.patch( + "api.admin.controller.quicksight.Configuration.quicksight_authorized_arns" + ) as mock_qs_arns: + mock_qs_arns.return_value = dict(primary=[], secondary=[], tertiary=[]) + ctrl = quicksight_fixture.manager.admin_quicksight_controller + assert ctrl.get_dashboard_names() == { + "names": ["primary", "secondary", "tertiary"] + }