diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 23a6fab6d..0403be079 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,14 +5,10 @@ This PR template is here to help ensure you're setup for success: --> **JIRA Ticket:** -[SOMEPROJECT-42](https://jira.cms.gov/browse/SOMEPROJECT-42) - -**User Story or Bug Summary:** - +[BB2-XXXX](https://jira.cms.gov/browse/BB2-XXXX) ### What Does This PR Do? - +Replace me. ### What Should Reviewers Watch For? - -If you're reviewing this PR, please check these things, in particular: +If you're reviewing this PR, please check for these things in particular: + -* TODO +### Validation + ### What Security Implications Does This PR Have? -Submitters should complete the following questionnaire: - -* If the answer to any of the questions below is **Yes**, then here's a link to the associated Security Impact Assessment (SIA), security checklist, or other similar document in Confluence: N/A. - * Does this PR add any new software dependencies? **Yes** or **No**. - * Does this PR modify or invalidate any of our security controls? **Yes** or **No**. - * Does this PR store or transmit data that was not stored or transmitted before? **Yes** or **No**. -* If the answer to any of the questions below is **Yes**, then please add a Security Engineer and ISSO as a reviewer, and note that this PR should not be merged unless/until he also approves it. - * Do you think this PR requires additional review of its security implications for other reasons? **Yes** or **No**. - - -### What Needs to Be Merged and Deployed Before this PR? - - +* Adds any new software dependencies +* Modifies any security controls +* Adds new transmission or storage of data +* Any other changes that could possibly affect security? -This PR cannot be either merged or deployed until the following pre-requisite changes have been fully deployed: +* [ ] Yes, one or more of the above security implications apply. This PR must not be merged without the ISSO or team security engineer's approval. -* CMSgov/some_repo#42 ### Any Migrations? @@ -83,31 +67,3 @@ Make sure to work with whoever is doing the deploy so they are aware of any migr * [ ] The migrations should be run AFTER the code is deployed * [ ] There is a more complicated migration plan (downtime, etc) * [ ] No migrations - - -### Submitter Checklist - - - -I have gone through and verified that...: - -* [ ] This PR is reasonably limited in scope, to help ensure that: - 1. It doesn't unnecessarily tie a bunch of disparate features, fixes, refactorings, etc. together. - 2. There isn't too much of a burden on reviewers. - 3. Any problems it causes have a small "blast radius". - 4. It'll be easier to rollback if that becomes necessary. -* [ ] I have named this PR and its branch such that they'll be automatically be linked to the (most) relevant Jira issue, per: . -* [ ] This PR includes any required documentation changes, including `README` updates and changelog / release notes entries. -* [ ] All new and modified code is appropriately commented, such that the what and why of its design would be reasonably clear to engineers, preferably ones unfamiliar with the project. -* [ ] All tech debt and/or shortcomings introduced by this PR are detailed in `TODO` and/or `FIXME` comments, which include a JIRA ticket ID for any items that require urgent attention. -* [ ] Reviews are requested from both: - * At least two other engineers on this project, at least one of whom is a senior engineer or owns the relevant component(s) here. - * Any relevant engineers on other projects (e.g. BFD, SLS, etc.). -* [ ] Any deviations from the other policies in the [DASG Engineering Standards](https://github.com/CMSgov/cms-oeda-dasg/blob/master/policies/engineering_standards.md) are specifically called out in this PR, above. - * Please review the standards every few months to ensure you're familiar with them. diff --git a/Dockerfile.selenium b/Dockerfile.selenium index 5c645fb35..83a68e10b 100755 --- a/Dockerfile.selenium +++ b/Dockerfile.selenium @@ -1,4 +1,4 @@ -FROM selenium/standalone-chrome +FROM seleniarm/standalone-chromium ENV PYTHONUNBUFFERED 1 USER root diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py index 777a94709..73dffe532 100644 --- a/apps/accounts/admin.py +++ b/apps/accounts/admin.py @@ -1,3 +1,4 @@ +from datetime import datetime from django.contrib import admin from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin @@ -9,7 +10,6 @@ UserIdentificationLabel, ) - admin.site.register(ActivationKey) admin.site.register(ValidPasswordResetKey) @@ -30,8 +30,36 @@ def queryset(self, request, queryset): return queryset -class UserAdmin(DjangoUserAdmin): +class ActiveAccountFilter(admin.SimpleListFilter): + title = "User activation status" + parameter_name = "status" + + def lookups(self, request, model_admin): + return [ + ("active", "Active"), + ("inactive_all", "Inactive"), + ("inactive_expired", "Inactive (expired activation key)") + ] + + def queryset(self, request, queryset): + if self.value() == "inactive_expired": + return queryset.filter( + is_active=False, + activationkey__key_status="expired", + ) | queryset.filter( + # Since the activation keys only reach "expired" status when they are + # used post-expiration, we need to check the "created" status as well + is_active=False, + activationkey__key_status="created", + activationkey__expires__lt=(datetime.today()), + ) + elif self.value() == "inactive_all": + return queryset.filter(is_active=False) + elif self.value() == "active": + return queryset.filter(is_active=True) + +class UserAdmin(DjangoUserAdmin): list_display = ( "username", "get_type", @@ -43,7 +71,7 @@ class UserAdmin(DjangoUserAdmin): "date_joined", ) - list_filter = (UserTypeFilter,) + list_filter = (UserTypeFilter, ActiveAccountFilter,) @admin.display( description="Type", diff --git a/apps/accounts/tests/test_password_reset_while_authenticated.py b/apps/accounts/tests/test_password_reset_while_authenticated.py index a42ea4ac0..36ccc6085 100644 --- a/apps/accounts/tests/test_password_reset_while_authenticated.py +++ b/apps/accounts/tests/test_password_reset_while_authenticated.py @@ -193,26 +193,15 @@ def test_password_change_reuse_validation(self): self.user = User.objects.get(username="fred") # get user again so that you can see password changed self.assertEquals(self.user.check_password("IchangedTHEpassword#123"), True) - # add 12 minutes to time to expire current password - StubDate.now = classmethod( - lambda cls, timezone: datetime.now().replace(tzinfo=pytz.UTC) + relativedelta(minutes=+12) - ) - self.client.logout() - form_data = {'username': 'fred', - 'password': 'IchangedTHEpassword#123'} - response = self.client.post(reverse('login'), form_data, follow=True) - self.assertContains(response, - ("Your password has expired, change password strongly recommended.")) - @override_switch('login', active=True) @mock.patch("apps.accounts.validators.datetime", StubDate) - def test_password_expire_not_affect_staff(self): + def test_no_password_expire(self): self.client.logout() - # add 20 minutes to time to show staff is not effected + # add 90 days to time to show expiration is removed StubDate.now = classmethod( - lambda cls, timezone: datetime.now().replace(tzinfo=pytz.UTC) + relativedelta(minutes=+20) + lambda cls, timezone: datetime.now().replace(tzinfo=pytz.UTC) + relativedelta(days=+90) ) - form_data = {'username': 'staff', + form_data = {'username': 'fred', 'password': 'foobarfoobarfoobar'} response = self.client.post(reverse('login'), form_data, follow=True) # assert account dashboard page @@ -222,8 +211,5 @@ def test_password_expire_not_affect_staff(self): ("The Developer Sandbox lets you register applications to get credentials")) def test_password_reuse_min_age_validator_args_check(self): - with self.assertRaisesRegex(ValueError, - (".*password_min_age < password_reuse_interval expected.*" - "password_expire < password_reuse_interval expected.*" - "password_min_age < password_expire expected.*")): - PasswordReuseAndMinAgeValidator(60 * 60 * 24 * 30, 60 * 60 * 24 * 10, 60 * 60 * 24 * 20) + with self.assertRaisesRegex(ValueError, ".*password_min_age < password_reuse_interval expected.*"): + PasswordReuseAndMinAgeValidator(60 * 60 * 24 * 30, 60 * 60 * 24 * 10) diff --git a/apps/accounts/validators.py b/apps/accounts/validators.py index 8e990ffb9..e04f1f521 100755 --- a/apps/accounts/validators.py +++ b/apps/accounts/validators.py @@ -86,7 +86,7 @@ class PasswordReuseAndMinAgeValidator(object): def __init__(self, password_min_age=60 * 60 * 24, password_reuse_interval=60 * 60 * 24 * 120, - password_expire=60 * 60 * 24 * 30): + password_expire=0): msg1 = "Invalid OPTIONS, password_min_age < password_reuse_interval expected, " \ "but having password_min_age({}) >= password_reuse_interval({})" @@ -96,14 +96,11 @@ def __init__(self, "but having password_expire({}) >= password_reuse_interval({})" check_opt_err = [] - if password_min_age > 0 and password_reuse_interval > 0 \ - and password_min_age > password_reuse_interval: + if 0 < password_reuse_interval < password_min_age: check_opt_err.append(msg1.format(password_min_age, password_reuse_interval)) - if password_expire > 0 and password_reuse_interval > 0 \ - and password_expire > password_reuse_interval: + if 0 < password_reuse_interval < password_expire: check_opt_err.append(msg2.format(password_expire, password_reuse_interval)) - if password_min_age > 0 and password_expire > 0 \ - and password_min_age > password_expire: + if 0 < password_expire < password_min_age: check_opt_err.append(msg3.format(password_min_age, password_expire)) if len(check_opt_err) > 0: raise ValueError(check_opt_err) @@ -234,8 +231,7 @@ def password_expired(self, user=None): except PastPassword.DoesNotExist: pass if passwds is not None and passwds.first() is not None: - if (datetime.now(timezone.utc) - - passwds.first().date_created).total_seconds() >= self.password_expire: + if (datetime.now(timezone.utc) - passwds.first().date_created).total_seconds() >= self.password_expire: # the elapsed time since last password change / create is more than password_expire passwd_expired = True return passwd_expired diff --git a/apps/accounts/views/login.py b/apps/accounts/views/login.py index 3ce1e4151..dc230b29a 100644 --- a/apps/accounts/views/login.py +++ b/apps/accounts/views/login.py @@ -21,11 +21,6 @@ def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def form_valid(self, form): - """ - Extend django login view to do password expire check - and redirect to password-change instead of user account home - """ - # auth_login(self.request, form.get_user()) response = super().form_valid(form) if response.status_code == 302: passwd_validators = get_default_password_validators() diff --git a/apps/authorization/views.py b/apps/authorization/views.py index 10749badd..cbcb44b51 100644 --- a/apps/authorization/views.py +++ b/apps/authorization/views.py @@ -1,3 +1,6 @@ +from datetime import datetime + +from django.db.models import Q from django.http import HttpResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt @@ -46,7 +49,10 @@ class AuthorizedGrants(viewsets.GenericViewSet, serializer_class = DataAccessGrantSerializer def get_queryset(self): - return DataAccessGrant.objects.select_related("application").filter(beneficiary=self.request.user) + return DataAccessGrant.objects.select_related("application").filter( + Q(expiration_date__gt=datetime.now()) | Q(expiration_date=None), + beneficiary=self.request.user + ) @method_decorator(csrf_exempt, name="dispatch") diff --git a/apps/dot_ext/forms.py b/apps/dot_ext/forms.py index fd25792fa..6d4345a9e 100644 --- a/apps/dot_ext/forms.py +++ b/apps/dot_ext/forms.py @@ -59,7 +59,9 @@ def __init__(self, user, *args, **kwargs): self.fields["name"].label = "Name*" self.fields["name"].required = True self.fields["client_type"].label = "Client Type*" + self.fields["client_type"].required = False self.fields["authorization_grant_type"].label = "Authorization Grant Type*" + self.fields["authorization_grant_type"].required = False self.fields["redirect_uris"].label = "Redirect URIs*" self.fields["logo_uri"].disabled = True @@ -86,29 +88,6 @@ class Meta: required_css_class = "required" def clean(self): - client_type = self.cleaned_data.get("client_type") - authorization_grant_type = self.cleaned_data.get("authorization_grant_type") - - msg = "" - validate_error = False - - # Validate choices - if not ( - client_type == "confidential" - and authorization_grant_type == "authorization-code" - ): - validate_error = True - msg += ( - "Only a confidential client and " - "authorization-code grant type are allowed at this time." - ) - - if validate_error: - msg_output = _(msg) - raise forms.ValidationError(msg_output) - else: - pass - return self.cleaned_data def clean_name(self): @@ -176,9 +155,11 @@ def clean_require_demographic_scopes(self): return require_demographic_scopes def save(self, *args, **kwargs): + self.instance.client_type = "confidential" + self.instance.authorization_grant_type = "authorization-code" app = self.instance # Only log agreement from a Register form - if app.agree and type(self) == CustomRegisterApplicationForm: + if app.agree and isinstance(self, CustomRegisterApplicationForm): logmsg = "%s agreed to %s for the application %s" % ( app.user, app.op_tos_uri, diff --git a/apps/dot_ext/models.py b/apps/dot_ext/models.py index 8d6d4a5d0..e8ffbaa10 100644 --- a/apps/dot_ext/models.py +++ b/apps/dot_ext/models.py @@ -33,6 +33,7 @@ from apps.capabilities.models import ProtectedCapability TEN_HOURS = _("for 10 hours") +THIRTEEN_MONTHS = _("for 13 months, until ") class Application(AbstractApplication): @@ -165,7 +166,7 @@ def access_end_date_text(self): return TEN_HOURS # no message displayed for RESEARCH_STUDY else: - return _("until ") + return THIRTEEN_MONTHS def access_end_date(self): if self.data_access_type == "THIRTEEN_MONTH": diff --git a/apps/dot_ext/tests/test_authorization.py b/apps/dot_ext/tests/test_authorization.py index 91c00e20c..3cc3ee263 100644 --- a/apps/dot_ext/tests/test_authorization.py +++ b/apps/dot_ext/tests/test_authorization.py @@ -1,5 +1,7 @@ import json import base64 +from time import strftime + import pytz from datetime import datetime from dateutil.relativedelta import relativedelta @@ -520,6 +522,62 @@ def test_refresh_with_one_time_access_retrieve_app_from_auth_header(self): ) self.assertEqual(response.status_code, 400) + @override_flag('limit_data_access', active=True) + def test_dag_expiration_exists(self): + assert flag_is_active('limit_data_access') + redirect_uri = 'http://localhost' + + # create a user + user = self._create_user('anna', '123456') + + # create an application and add capabilities + application = self._create_application( + 'an app', + grant_type=Application.GRANT_AUTHORIZATION_CODE, + client_type=Application.CLIENT_CONFIDENTIAL, + redirect_uris=redirect_uri, + data_access_type="THIRTEEN_MONTH", + ) + capability_a = self._create_capability('Capability A', []) + application.scope.add(capability_a) + + # create a data access grant + expiration_date = datetime.now() + relativedelta(months=+13) + dag = DataAccessGrant(beneficiary=user, application=application, expiration_date=expiration_date) + dag.save() + + # user logs in + request = HttpRequest() + self.client.login(request=request, username='anna', password='123456') + + # post the authorization form with only one scope selected + payload = { + 'client_id': application.client_id, + 'response_type': 'code', + 'redirect_uri': redirect_uri, + 'scope': ['capability-a'], + 'expires_in': 86400, + 'allow': True, + } + response = self.client.post(reverse('oauth2_provider:authorize'), data=payload) + self.client.logout() + + # now extract the authorization code and use it to request an access_token + query_dict = parse_qs(urlparse(response['Location']).query) + authorization_code = query_dict.pop('code') + token_request_data = { + 'grant_type': 'authorization_code', + 'code': authorization_code, + 'redirect_uri': redirect_uri, + 'client_id': application.client_id, + 'client_secret': application.client_secret_plain, + } + c = Client() + response = c.post('/v1/o/token/', data=token_request_data) + tkn = response.json() + expiration_date_string = strftime('%Y-%m-%d %H:%M:%SZ', expiration_date.timetuple()) + self.assertEqual(tkn["access_grant_expiration"][:-4], expiration_date_string[:-4]) + def test_refresh_with_revoked_token(self): redirect_uri = 'http://localhost' # create a user diff --git a/apps/dot_ext/tests/test_form_application.py b/apps/dot_ext/tests/test_form_application.py index eeb6e206d..2ff91eda3 100644 --- a/apps/dot_ext/tests/test_form_application.py +++ b/apps/dot_ext/tests/test_form_application.py @@ -258,33 +258,6 @@ def test_update_form_edit(self): form = CustomRegisterApplicationForm(user, passing_app_fields) self.assertTrue(form.is_valid()) - # Test client_type = 'confidential' and authorization_grant_type = 'implicit' not allowed. - data = passing_app_fields - data['client_type'] = 'confidential' - data['authorization_grant_type'] = 'implicit' - form = CustomRegisterApplicationForm(user, data) - self.assertFalse(form.is_valid()) - self.assertIn('Only a confidential client and authorization-code grant type are allowed at this time.', - str(form.errors.get('__all__'))) - - # Test client_type = 'public' and grant_type = 'authorization-code' not allowed. - data = passing_app_fields - data['client_type'] = 'public' - data['authorization_grant_type'] = 'authorization-code' - form = CustomRegisterApplicationForm(user, data) - self.assertFalse(form.is_valid()) - self.assertIn('Only a confidential client and authorization-code grant type are allowed at this time.', - str(form.errors.get('__all__'))) - - # Test client_type = 'public' and grant_type = 'implicit' not allowed. - data = passing_app_fields - data['client_type'] = 'public' - data['authorization_grant_type'] = 'implicit' - form = CustomRegisterApplicationForm(user, data) - self.assertFalse(form.is_valid()) - self.assertIn('Only a confidential client and authorization-code grant type are allowed at this time.', - str(form.errors.get('__all__'))) - def test_create_applications_with_logo(self): """ regression test: BB2-66: Fix-logo-display-in-Published-Applications-API diff --git a/apps/dot_ext/tests/test_views.py b/apps/dot_ext/tests/test_views.py index d86cbca6f..8543d2c1b 100644 --- a/apps/dot_ext/tests/test_views.py +++ b/apps/dot_ext/tests/test_views.py @@ -1,5 +1,7 @@ import json import base64 +from datetime import date, timedelta + from django.conf import settings from django.http import HttpRequest from django.urls import reverse @@ -465,6 +467,43 @@ def test_get_tokens_success(self): ] self.assertEqual(result, expected) + # Check tokens endpoint doesn't return expired + application2 = self._create_application( + "an expired app", + grant_type=Application.GRANT_AUTHORIZATION_CODE, + redirect_uris="http://example.it", + user=anna + ) + DataAccessGrant.objects.update_or_create( + beneficiary=anna, application=application2, expiration_date=date.today() - timedelta(days=1) + ) + response = self.client.get( + "/v1/o/tokens/", + headers={ + "authorization": self._create_authorization_header( + application.client_id, application.client_secret_plain + ), + "x-authentication": self._create_authentication_header(self.test_uuid), + }, + ) + self.assertEqual(response.status_code, 200) + result = response.json() + expected = [ + { + "id": result[0]["id"], + "user": anna.id, + "application": { + "id": application.id, + "name": "an app", + "logo_uri": "", + "tos_uri": "", + "policy_uri": "", + "contacts": "", + }, + } + ] + self.assertEqual(result, expected) + def test_get_tokens_on_inactive_app(self): anna = self._create_user(self.test_username, "123456") # create a couple of capabilities diff --git a/apps/dot_ext/utils.py b/apps/dot_ext/utils.py index ffbf04bc1..a9e1242e0 100644 --- a/apps/dot_ext/utils.py +++ b/apps/dot_ext/utils.py @@ -95,7 +95,8 @@ def validate_app_is_active(request): Utility function to check that an application is an active, valid application. This method will pull the application from the - request and then check the active flag. + request and then check the active flag and the + data access grant (dag) validity. RETURN: application or None """ diff --git a/apps/dot_ext/views/authorization.py b/apps/dot_ext/views/authorization.py index 10ba6a140..770fa395e 100644 --- a/apps/dot_ext/views/authorization.py +++ b/apps/dot_ext/views/authorization.py @@ -1,13 +1,18 @@ +import json import logging +from datetime import datetime, timedelta +from time import strftime + import waffle from waffle import get_waffle_flag_model -from django.http.response import HttpResponseBadRequest +from django.http.response import HttpResponse, HttpResponseBadRequest from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.decorators.debug import sensitive_post_parameters from oauth2_provider.exceptions import OAuthToolkitError +from oauth2_provider.views.base import app_authorized, get_access_token_model from oauth2_provider.views.base import AuthorizationView as DotAuthorizationView from oauth2_provider.views.base import TokenView as DotTokenView from oauth2_provider.views.base import RevokeTokenView as DotRevokeTokenView @@ -37,7 +42,7 @@ validate_app_is_active, json_response_from_oauth2_error, ) - +from ...authorization.models import DataAccessGrant log = logging.getLogger(bb2logging.HHS_SERVER_LOGNAME_FMT.format(__name__)) @@ -292,11 +297,48 @@ class TokenView(DotTokenView): @method_decorator(sensitive_post_parameters("password")) def post(self, request, *args, **kwargs): try: - validate_app_is_active(request) + app = validate_app_is_active(request) except (InvalidClientError, InvalidGrantError) as error: return json_response_from_oauth2_error(error) - return super().post(request, args, kwargs) + url, headers, body, status = self.create_token_response(request) + + if status == 200: + body = json.loads(body) + access_token = body.get("access_token") + + dag_expiry = "" + if access_token is not None: + token = get_access_token_model().objects.get( + token=access_token) + app_authorized.send( + sender=self, request=request, + token=token) + + if app.data_access_type == "THIRTEEN_MONTH": + try: + dag = DataAccessGrant.objects.get( + beneficiary=token.user, + application=app + ) + if dag.expiration_date is not None: + dag_expiry = strftime('%Y-%m-%d %H:%M:%SZ', dag.expiration_date.timetuple()) + except DataAccessGrant.DoesNotExist: + dag_expiry = "" + + elif app.data_access_type == "ONE_TIME": + expires_at = datetime.utcnow() + timedelta(seconds=body['expires_in']) + dag_expiry = expires_at.strftime('%Y-%m-%d %H:%M:%SZ') + elif app.data_access_type == "RESEARCH_STUDY": + dag_expiry = "" + + body['access_grant_expiration'] = dag_expiry + body = json.dumps(body) + + response = HttpResponse(content=body, status=status) + for k, v in headers.items(): + response[k] = v + return response @method_decorator(csrf_exempt, name="dispatch") diff --git a/apps/integration_tests/common_utils.py b/apps/integration_tests/common_utils.py index d23cc8a44..1ebfd4290 100755 --- a/apps/integration_tests/common_utils.py +++ b/apps/integration_tests/common_utils.py @@ -1,4 +1,5 @@ import jsonschema +import re from jsonschema import validate @@ -11,3 +12,26 @@ def validate_json_schema(schema, content): print("jsonschema.exceptions.ValidationError: ", e) return False return True + + +def extract_href_from_html(html): + # Regular expression patterns + # title_pattern = r'title="([^"]*)"' + href_pattern = r'href="([^"]*)"' + + # Search for title and href attributes + # title_match = re.search(title_pattern, html) + href_match = re.search(href_pattern, html) + + # title = title_match.group(1) if title_match else None + href = href_match.group(1) if href_match else None + + return href + + +def extract_last_part_of_url(url): + # Split the URL by '/' and get the last part + parts = url.rstrip('/').split('/') + last_part = parts[-1] + + return last_part diff --git a/apps/integration_tests/selenium_cases.py b/apps/integration_tests/selenium_cases.py index ba29d3c75..ff104881b 100755 --- a/apps/integration_tests/selenium_cases.py +++ b/apps/integration_tests/selenium_cases.py @@ -41,6 +41,7 @@ class Action(Enum): LNK_TXT_GET_TOKEN_PKCE_V1 = "Get a Sample Authorization Token (PKCE Enabled)" LNK_TXT_GET_TOKEN_PKCE_V2 = "Get a Sample Authorization Token for v2 (PKCE Enabled)" LNK_TXT_AUTH_AS_BENE = "Authorize as a Beneficiary" +LNK_TXT_AUTH_AS_BENE_SPANISH = "Authorize as a Beneficiary (Spanish)" LNK_TXT_RESTART_TESTCLIENT = "restart testclient" TAG_FOR_AUTHORIZE_LINK = "pre" # FHIR search result bundle pagination @@ -74,7 +75,7 @@ class Action(Enum): # email notification subjects USER_ACCT_ACTIVATION_EMAIL_SUBJ = "Subject: Verify Your Blue Button 2.0 Developer Sandbox Account" USER_ACCT_1ST_APP_EMAIL_SUBJ = "Subject: Congrats on Registering Your First Application!" -USER_ACCT_ACTIVATION_KEY_PREFIX = "Activation Key: " +USER_ACCT_ACTIVATION_KEY_PREFIX = 'title="Verify Your Email"' APP_1ST_API_CALL_EMAIL_SUBJ = "Subject: Congrats on Making Your First API Call" # create user account form fields @@ -110,12 +111,15 @@ class Action(Enum): # language and localization checking AUTH_SCREEN_ID_LANG = "connect_app" AUTH_SCREEN_ID_END_DATE = "permission_end_date" -AUTH_SCREEN_ES_TXT = "Conectar los datos de sus reclamos de Medicare" +AUTH_SCREEN_ID_EXPIRE_INFO = "permission_expire_info" +AUTH_SCREEN_ES_TXT = "Desea compartir sus datos de Medicare" AUTH_SCREEN_EN_TXT = "Connect your Medicare claims" +AUTH_SCREEN_EN_EXPIRE_INFO_TXT = "TestApp will have access to your data for 13 months, until" +AUTH_SCREEN_ES_EXPIRE_INFO_TXT = "TestApp tendrá acceso a sus datos durante 13 meses, hasta el" # regex for date formats -AUTH_SCREEN_ES_DATE_FORMAT = "^\\d{1,2} de \\w+ de \\d{4}" +AUTH_SCREEN_ES_DATE_FORMAT = "^(?P\\d{1,2}) de (?P\\w+) de (?P\\d{4})" # Django en locale date format is 3 letter abbrev plus period or full month name (e.g. March, May) -AUTH_SCREEN_EN_DATE_FORMAT = "^(\\w{3}\\.|\\w+) \\d{1,2}, \\d{4}" +AUTH_SCREEN_EN_DATE_FORMAT = "^(?P\\w{3}\\.|\\w+) (?P\\d{1,2}), (?P\\d{4})" SLSX_LOGIN_BUTTON_SPANISH = "Entrar" # app form @@ -150,6 +154,9 @@ class Action(Enum): # Below works for old auth screen BTN_ID_RADIO_NOT_SHARE = "label:nth-child(5)" +# Supported Locale +EN_US = "en_us" +ES_ES = "es_es" # API versions API_V2 = "v2" @@ -274,7 +281,7 @@ class Action(Enum): }, ] -SEQ_AUTHORIZE_START = [ +SEQ_REACH_AUTHORIZE_BTN = [ { "display": "Load BB2 Landing Page ...", "action": Action.LOAD_PAGE, @@ -285,6 +292,10 @@ class Action(Enum): "display": "Click link to get sample token v1/v2", "action": Action.GET_SAMPLE_TOKEN_START, }, +] + +SEQ_AUTHORIZE_START = [ + {"sequence": SEQ_REACH_AUTHORIZE_BTN}, { "display": "Click link 'Authorize as a Beneficiary' - start authorization", "action": Action.FIND_CLICK, @@ -292,6 +303,15 @@ class Action(Enum): }, ] +SEQ_AUTHORIZE_START_SPANISH = [ + {"sequence": SEQ_REACH_AUTHORIZE_BTN}, + { + "display": "Click link 'Authorize as a Beneficiary (Spanish)' - start authorization using medicare login in Spanish", + "action": Action.FIND_CLICK, + "params": [30, By.LINK_TEXT, LNK_TXT_AUTH_AS_BENE_SPANISH] + }, +] + SEQ_AUTHORIZE_RESTART = [ CLICK_RESTART_TESTCLIENT, { @@ -543,11 +563,38 @@ class Action(Enum): CLICK_RADIO_NOT_SHARE_NEW_PERM_SCREEN, CLICK_AGREE_ACCESS, {"sequence": SEQ_QUERY_FHIR_RESOURCES_NO_DEMO} + ], + "authorize_lang_english_button": [ + {"sequence": SEQ_AUTHORIZE_START}, + CALL_LOGIN, + WAIT_SECONDS, + WAIT_SECONDS, + # check the title + { + "display": "Check for authorization screen language in English", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_LANG, AUTH_SCREEN_EN_TXT] + }, + # now check the expiration info section + { + "display": "Check for authorization screen expire info in English", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_EXPIRE_INFO, AUTH_SCREEN_EN_EXPIRE_INFO_TXT] + }, + { + "display": "Check en_US date format and validate", + "action": Action.CHECK_DATE_FORMAT, + "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_EN_DATE_FORMAT, EN_US] + }, + # the 'approve' and 'deny' button click not using locale based text + # so it is lang agnostic + CLICK_AGREE_ACCESS ] } SPANISH_TESTS = { "toggle_language": [ + # kick off default test client {"sequence": SEQ_AUTHORIZE_START}, CALL_LOGIN, # Wait to make sure we're logged in because login page also has Spanish link @@ -559,11 +606,6 @@ class Action(Enum): "action": Action.CONTAIN_TEXT, "params": [20, By.ID, AUTH_SCREEN_ID_LANG, AUTH_SCREEN_ES_TXT] }, - { - "display": "Check Spanish date format", - "action": Action.CHECK_DATE_FORMAT, - "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_ES_DATE_FORMAT] - }, CLICK_ENGLISH, { "display": "Check for language change to English", @@ -571,27 +613,77 @@ class Action(Enum): "params": [20, By.ID, AUTH_SCREEN_ID_LANG, AUTH_SCREEN_EN_TXT] }, { - "display": "Check English date format", + "display": "Check for authorization screen access grant expire info in English", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_EXPIRE_INFO, AUTH_SCREEN_EN_EXPIRE_INFO_TXT] + }, + { + "display": "Check English date format and validate", "action": Action.CHECK_DATE_FORMAT, - "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_EN_DATE_FORMAT] + "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_EN_DATE_FORMAT, EN_US] }, CLICK_AGREE_ACCESS ], "authorize_lang_param": [ + # direct medicare login by inject a lang=es at the end of the url {"sequence": SEQ_AUTHORIZE_LANG_PARAM_START}, { "display": "Check for Medicare.gov login page already in Spanish", "action": Action.CONTAIN_TEXT, "params": [20, By.ID, SLSX_CSS_BUTTON, SLSX_LOGIN_BUTTON_SPANISH] }, + # note, for now CALL_LOGIN does not use locale based text to look up elements + # so it is lang agnostic + CALL_LOGIN, + WAIT_SECONDS, + WAIT_SECONDS, + # check the title + { + "display": "Check for authorization screen language already in Spanish", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_LANG, AUTH_SCREEN_ES_TXT] + }, + # now check the expiration info section + { + "display": "Check for authorization screen expire info in Spanish", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_EXPIRE_INFO, AUTH_SCREEN_ES_EXPIRE_INFO_TXT] + }, + { + "display": "Check Spanish date format and validate", + "action": Action.CHECK_DATE_FORMAT, + "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_ES_DATE_FORMAT, ES_ES] + }, + # the 'approve' and 'deny' button click not using locale based text + # so it is lang agnostic + CLICK_AGREE_ACCESS + ], + "authorize_lang_spanish_button": [ + {"sequence": SEQ_AUTHORIZE_START_SPANISH}, + # note, CALL_LOGIN does not use locale based text to look up elements + # so it is lang agnostic CALL_LOGIN, WAIT_SECONDS, WAIT_SECONDS, + # check the title { "display": "Check for authorization screen language already in Spanish", "action": Action.CONTAIN_TEXT, "params": [20, By.ID, AUTH_SCREEN_ID_LANG, AUTH_SCREEN_ES_TXT] }, + # now check the expiration info section + { + "display": "Check for authorization screen expire info in Spanish", + "action": Action.CONTAIN_TEXT, + "params": [20, By.ID, AUTH_SCREEN_ID_EXPIRE_INFO, AUTH_SCREEN_ES_EXPIRE_INFO_TXT] + }, + { + "display": "Check Spanish date format and validate", + "action": Action.CHECK_DATE_FORMAT, + "params": [20, By.ID, AUTH_SCREEN_ID_END_DATE, AUTH_SCREEN_ES_DATE_FORMAT, ES_ES] + }, + # the 'approve' and 'deny' button click not using locale based text + # so it is lang agnostic CLICK_AGREE_ACCESS ] } diff --git a/apps/integration_tests/selenium_generic.py b/apps/integration_tests/selenium_generic.py index 82b39e385..2f5c0d967 100644 --- a/apps/integration_tests/selenium_generic.py +++ b/apps/integration_tests/selenium_generic.py @@ -2,11 +2,13 @@ import time import re +from datetime import datetime, timedelta from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.common.keys import Keys +from .common_utils import extract_href_from_html, extract_last_part_of_url from .selenium_cases import ( Action, @@ -21,9 +23,13 @@ SEQ_LOGIN_MSLSX, SEQ_LOGIN_SLSX, PROD_URL, + ES_ES, ) LOG_FILE = "./docker-compose/tmp/bb2_email_to_stdout.log" +EN_MONTH_ABBR = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May.', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'] +ES_MONTH_NAME = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', + 'octubre', 'noviembre', 'diciembre'] # class SeleniumGenericTests(TestCase): @@ -96,9 +102,10 @@ def _validate_email_content(self, subj_line, key_line_prefix, **kwargs): if r.startswith(subj_line): # print("SUBJ: {}".format(r)) email_subj_cnt += 1 - elif key_line_prefix is not None and r.startswith(key_line_prefix): + elif key_line_prefix is not None and key_line_prefix in r: # print("KEY: {}".format(r)) - ak = r.split(key_line_prefix)[1] + href = extract_href_from_html(r) + ak = extract_last_part_of_url(href) key_cnt += 1 else: pass @@ -163,10 +170,39 @@ def _check_page_content(self, timeout_sec, by, by_expr, content_txt, **kwargs): elem = self._find_and_return(timeout_sec, by, by_expr, **kwargs) assert content_txt in elem.text - def _check_date_format(self, timeout_sec, by, by_expr, format, **kwargs): + def _check_date_format(self, timeout_sec, by, by_expr, format, lang, **kwargs): elem = self._find_and_return(timeout_sec, by, by_expr, **kwargs) pattern = re.compile(format) - assert pattern.match(elem.text) + m = pattern.match(elem.text) + print("date: " + elem.text) + try: + day = m.group('day') + month = m.group('month') + year = m.group('year') + month_num = -1 + try: + if lang == ES_ES: + # for ES_ES, month is full name + # locale.setlocale(locale.LC_ALL, ES_ES) - choose not to use locale package (it might be thread unsafe) + # use a pre-built array to do month name -> month num mapping + month_num = ES_MONTH_NAME.index(month) + else: + # for EN_US, month is abbr + month_num = EN_MONTH_ABBR.index(month) + except ValueError as v: + print(v) + assert 1 < 0, "Invalid month name or abbr." + month + print("year: " + year + ", month: " + str(month_num) + ", day: " + day) + if month_num >= 0: + expire_date = datetime(int(year), month_num + 1, int(day)) + # 395 is 13 month, validate it's in that range + assert (expire_date - datetime.today()) > timedelta(days=394), "Expiration date not 13 month away." + else: + assert 1 < 0, "Invalid month name or name abbr." + month + except IndexError as e: + # bad date value + print(e) + assert 1 < 0, "Malformed date value" + elem.text def _copy_link_and_load_with_param(self, timeout_sec, by, by_expr, **kwargs): elem = WebDriverWait(self.driver, timeout_sec).until(EC.visibility_of_element_located((by, by_expr))) diff --git a/apps/integration_tests/selenium_spanish_tests.py b/apps/integration_tests/selenium_spanish_tests.py index 81606d55e..3aa040383 100644 --- a/apps/integration_tests/selenium_spanish_tests.py +++ b/apps/integration_tests/selenium_spanish_tests.py @@ -3,6 +3,7 @@ API_V2, SPANISH_TESTS ) + USE_NEW_PERM_SCREEN = "true" ''' @@ -21,19 +22,44 @@ def test_toggle_language_and_date_format(self): test_name = "toggle_language" api_ver = API_V2 self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, True) - self._play(SPANISH_TESTS[test_name], step, api_ver=api_ver) - self._testclient_home() + if not self.use_mslsx: + self._play(SPANISH_TESTS[test_name], step, api_ver=api_ver) + self._testclient_home() + else: + assert True, "Skip test " + test_name + " - does not applicable to mslsx (mock login)." self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, False) ''' Test lang param support on the authorize end point via the built in testclient using the Selenium web driver (Chrome) + inject lang=es before direct to login url ''' def test_authorize_lang_param(self): step = [0] test_name = "authorize_lang_param" api_ver = API_V2 self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, True) - self._play(SPANISH_TESTS[test_name], step, api_ver=api_ver) + if not self.use_mslsx: + self._play(SPANISH_TESTS[test_name], step, api_ver=api_ver) + self._testclient_home() + else: + assert True, "Skip test " + test_name + " - does not applicable to mslsx (mock login)." + self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, False) + + ''' + Test lang param support on the authorize end point via the built in + testclient using the Selenium web driver (Chrome) + direct to login url with lang=es by click on "Authorize as beneficiary (Spanish)" button + ''' + def test_authorize_lang_spanish_button(self): + step = [0] + test_name = "authorize_lang_spanish_button" + api_ver = API_V2 + self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, True) + if USE_NEW_PERM_SCREEN == "true": + # the validation of expire date etc. only applicable to new perm screen + self._play(SPANISH_TESTS[test_name], step, api_ver=api_ver) + else: + print("Skip test " + test_name + " - only for new perm screen.") self._testclient_home() self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, False) diff --git a/apps/integration_tests/selenium_tests.py b/apps/integration_tests/selenium_tests.py index 87035812b..4422ab286 100755 --- a/apps/integration_tests/selenium_tests.py +++ b/apps/integration_tests/selenium_tests.py @@ -91,3 +91,21 @@ def test_auth_grant_w_no_demo_v2(self): self._play(TESTS[test_name], step, api_ver=api_ver) self._testclient_home() self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, False) + + ''' + Test lang param support on the authorize end point via the built in + testclient using the Selenium web driver (Chrome) + direct to login url with lang=en by click on "Authorize as beneficiary" button + ''' + def test_authorize_lang_english_button(self): + step = [0] + test_name = "authorize_lang_english_button" + api_ver = API_V2 + self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, True) + if USE_NEW_PERM_SCREEN == "true": + # the validation of expire date etc. only applicable to new perm screen + self._play(TESTS[test_name], step, api_ver=api_ver) + else: + print("Skip test " + test_name + " - only for new perm screen.") + self._testclient_home() + self._print_testcase_banner(test_name, api_ver, step[0], self.use_mslsx, False) diff --git a/apps/logging/tests/test_loggers_management_command.py b/apps/logging/tests/test_loggers_management_command.py index 6cdfcef47..e7458914d 100644 --- a/apps/logging/tests/test_loggers_management_command.py +++ b/apps/logging/tests/test_loggers_management_command.py @@ -79,7 +79,7 @@ def _debug_show_value_differences(self, dict_a, dict_b): else: a = None b = dict_b.get(key, None) - if a != b and type(b) == int: + if a != b and isinstance(b, int): print("\"{}\": {}, # {} -> {}".format(key, b, a, b)) print("=========================================================") print("") diff --git a/apps/testclient/templates/authorize.html b/apps/testclient/templates/authorize.html index 97754f1f8..ffb6beacb 100644 --- a/apps/testclient/templates/authorize.html +++ b/apps/testclient/templates/authorize.html @@ -35,7 +35,9 @@

You'll need sample beneficiary credentials to lo diff --git a/docker-compose.selenium.remote.yml b/docker-compose.selenium.remote.yml index 95d65b468..683729f6d 100644 --- a/docker-compose.selenium.remote.yml +++ b/docker-compose.selenium.remote.yml @@ -1,11 +1,9 @@ -version: '3' - services: selenium-remote-tests: build: context: ./ dockerfile: Dockerfile.selenium - command: pytest ./apps/integration_tests/selenium_tests.py + command: pytest ./apps/integration_tests/selenium_tests.py ./apps/integration_tests/selenium_spanish_tests.py env_file: - docker-compose/selenium-env-vars.env volumes: diff --git a/docker-compose.selenium.yml b/docker-compose.selenium.yml index f7bd11ee9..cd3cb1c46 100755 --- a/docker-compose.selenium.yml +++ b/docker-compose.selenium.yml @@ -1,11 +1,9 @@ -version: '3' - services: selenium-tests: build: context: ./ dockerfile: Dockerfile.selenium - command: pytest ./apps/integration_tests/selenium_tests.py + command: pytest ./apps/integration_tests/selenium_tests.py ./apps/integration_tests/selenium_spanish_tests.py env_file: - docker-compose/selenium-env-vars.env volumes: diff --git a/docker-compose.yml b/docker-compose.yml index f78b131a4..73ea528e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: msls: build: ./dev-local diff --git a/docker-compose/run_selenium_tests_local.sh b/docker-compose/run_selenium_tests_local.sh index e2ea3c170..f12659efc 100755 --- a/docker-compose/run_selenium_tests_local.sh +++ b/docker-compose/run_selenium_tests_local.sh @@ -74,7 +74,7 @@ set -e -u -o pipefail export USE_MSLSX=true export USE_NEW_PERM_SCREEN=false export SERVICE_NAME="selenium-tests" -export TESTS_LIST="./apps/integration_tests/selenium_tests.py" +export TESTS_LIST="./apps/integration_tests/selenium_tests.py ./apps/integration_tests/selenium_spanish_tests.py" export DJANGO_SETTINGS_MODULE="hhs_oauth_server.settings.dev" export BB2_SERVER_STD2FILE="" @@ -94,6 +94,8 @@ while getopts "hp" option; do esac done +eval last_arg=\$$# + set_msls # Parse command line option @@ -101,19 +103,19 @@ if [ $# -eq 0 ] then echo "Use Mock SLS for identity service." else - echo $1 - if [[ $1 != "slsx" && $1 != "mslsx" && $1 != "logit" && $1 != "account" ]] + echo $last_arg + if [[ $last_arg != "slsx" && $last_arg != "mslsx" && $last_arg != "logit" && $last_arg != "account" ]] then - echo "Invalid argument: " $1 + echo "Invalid argument: " $last_arg display_usage exit 1 else - if [[ $1 == "slsx" ]] + if [[ $last_arg == "slsx" ]] then export USE_MSLSX=false set_slsx fi - if [[ $1 == "logit" ]] + if [[ $last_arg == "logit" ]] then # cleansing log file before run rm -rf ./docker-compose/tmp/ @@ -122,7 +124,7 @@ else export TESTS_LIST="./apps/integration_tests/logging_tests.py::TestLoggings::test_auth_fhir_flows_logging" export DJANGO_LOG_JSON_FORMAT_PRETTY=False fi - if [[ $1 == "account" ]] + if [[ $last_arg == "account" ]] then # cleansing log file before run rm -rf ./docker-compose/tmp/ diff --git a/docker-compose/run_selenium_tests_remote.sh b/docker-compose/run_selenium_tests_remote.sh index 611788a6d..274bc2c81 100755 --- a/docker-compose/run_selenium_tests_remote.sh +++ b/docker-compose/run_selenium_tests_remote.sh @@ -51,7 +51,7 @@ set -e -u -o pipefail export USE_NEW_PERM_SCREEN=false export SERVICE_NAME="selenium-tests-remote" # TODO optionally add the Spanish selenium tests here if desired -export TESTS_LIST="./apps/integration_tests/selenium_tests.py" +export TESTS_LIST="./apps/integration_tests/selenium_tests.py ./apps/integration_tests/selenium_spanish_tests.py" # BB2 service end point default (SBX) export HOSTNAME_URL="https://sandbox.bluebutton.cms.gov/" @@ -71,6 +71,8 @@ done eval last_arg=\$$# +echo "last arg: " $last_arg + if [[ -n ${last_arg} ]] then case "${last_arg}" in @@ -84,7 +86,7 @@ then export HOSTNAME_URL="https://test.bluebutton.cms.gov/" ;; *) - if [[ ${last_arg} == 'http*' ]] + if [[ ${last_arg} == 'http'* ]] then export HOSTNAME_URL=${last_arg} else diff --git a/hhs_oauth_server/request_logging.py b/hhs_oauth_server/request_logging.py index 4e4469e10..561a3567e 100644 --- a/hhs_oauth_server/request_logging.py +++ b/hhs_oauth_server/request_logging.py @@ -432,7 +432,7 @@ def to_dict(self): """ --- Logging items from a FHIR type response --- """ - if type(self.response) == Response and isinstance(self.response.data, dict): + if isinstance(self.response, Response) and isinstance(self.response.data, dict): self.log_msg["fhir_bundle_type"] = self.response.data.get("type", None) self.log_msg["fhir_resource_id"] = self.response.data.get("id", None) self.log_msg["fhir_resource_type"] = self.response.data.get( diff --git a/hhs_oauth_server/settings/base.py b/hhs_oauth_server/settings/base.py index 1f5aea2b3..ed6312d9c 100644 --- a/hhs_oauth_server/settings/base.py +++ b/hhs_oauth_server/settings/base.py @@ -77,8 +77,7 @@ "password_min_age": 60 * 5, # password reuse interval in seconds (365 days) "password_reuse_interval": 60 * 60 * 24 * 365, - # password expire in seconds (60 days) - "password_expire": 60 * 60 * 24 * 60, + "password_expire": 0, }, }, { diff --git a/hhs_oauth_server/settings/test.py b/hhs_oauth_server/settings/test.py index 4be7a5df3..9f84e5c9c 100644 --- a/hhs_oauth_server/settings/test.py +++ b/hhs_oauth_server/settings/test.py @@ -63,8 +63,7 @@ 'password_min_age': 60, # password reuse interval in seconds (50 minutes) 'password_reuse_interval': 3000, - # password expire in seconds (10 minutes) - 'password_expire': 600, + 'password_expire': 0, } }, { diff --git a/splunk/authorization_flow_dashboard.xml b/splunk/authorization_flow_dashboard.xml index 5e2e97d4a..1e8ab3eb7 100644 --- a/splunk/authorization_flow_dashboard.xml +++ b/splunk/authorization_flow_dashboard.xml @@ -1,4 +1,4 @@ -
+ Dashboard panels related to the authorization flow log tracing
@@ -205,7 +205,7 @@ 1 - index=bluebutton source="/var/log/pyapps/perf_mon.log*" host=$bbEnv$ env=$bbEnvLabel$ $authAppNameExpr$ | spath "message.path" | search "message.path"="/$apiVersionsPattern$/o/authorize/" message.req_qparam_lang="es-mx" | fields time message.auth_uuid message.auth_app_name message.ip_addr message.response_code message.path message.auth_pkce_method message.req_qparam_lang + index=bluebutton source="/var/log/pyapps/perf_mon.log*" host=$bbEnv$ env=$bbEnvLabel$ $authAppNameExpr$ | spath "message.path" | search "message.path"="/$apiVersionsPattern$/o/authorize/" message.req_qparam_lang="es*" | fields time message.auth_uuid message.auth_app_name message.ip_addr message.response_code message.path message.auth_pkce_method message.req_qparam_lang $t_local.earliest$ $t_local.latest$ 1 @@ -364,23 +364,8 @@ - - 13. Spanish Language Authorization - - - - $result.count$ - - search "message.req_qparam_lang"="es-mx" | stats count - - - - - - - Initial Authorization Request / Authorization Completed % @@ -395,6 +380,37 @@ + + Spanish Language Authorization Count + + + + $result.count$ + + stats count + + + + + + search?q=index%3Dbluebutton%20source%3D%22%2Fvar%2Flog%2Fpyapps%2Fperf_mon.log*%22%7C%20spath%20%22message.req_qparam_lang%22%20%7C%20search%20%22message.path%22%3D%22%2Fv*%2Fo%2Fauthorize%2F*%22%20message.req_qparam_lang%3D%22es*%22%20%7C%20fields%20time%20message.auth_uuid%20message.auth_app_name%20message.ip_addr%20message.response_code%20message.path%20message.auth_pkce_method%20message.req_qparam_lang%20%7C%20stats%20count&earliest=$t_local.earliest$&latest=$t_local.latest$ + + + + + + Spanish Authorization Request / Total Authorization Request % + + | makeresults | eval Total1=$tokEpCount$, Total2=$tokEpCountLang$ | eval percent= round((Total2/Total1)*100,1) | table percent + 1 + + + + + + + + @@ -462,7 +478,7 @@ $result.count$ - stats count + search "message.response_code"="302" | stats count @@ -1359,7 +1375,7 @@ - SUCCESSFUL Events (response_code=302) + SUCCESSFUL Events (response_code="302") @@ -1567,7 +1583,7 @@ - SUCCESSFUL Events (response_code=302) + SUCCESSFUL Events (response_code="302") diff --git a/templates/design_system/locale/es/LC_MESSAGES/django.mo b/templates/design_system/locale/es/LC_MESSAGES/django.mo index f78b6363b..6d4adb696 100644 Binary files a/templates/design_system/locale/es/LC_MESSAGES/django.mo and b/templates/design_system/locale/es/LC_MESSAGES/django.mo differ diff --git a/templates/design_system/locale/es/LC_MESSAGES/django.po b/templates/design_system/locale/es/LC_MESSAGES/django.po index ed422377e..d304b1d6e 100644 --- a/templates/design_system/locale/es/LC_MESSAGES/django.po +++ b/templates/design_system/locale/es/LC_MESSAGES/django.po @@ -4,6 +4,7 @@ # TRANSLATORS: text and punctuation after 'msgid' is the English text requiring translation. # The translation text should go within the quotation marks following the 'msgstr' identifier. # Please add/maintain all punctuation and whitespce found in the original text. +# When updating this file, run `django-admin compilemessages` for the changes to be compiled. #, fuzzy msgid "" msgstr "" @@ -70,11 +71,11 @@ msgstr "" #: templates/design_system/new_authorize_v2.html:56 msgid "Connect your Medicare claims data to " -msgstr "¿Conectar los datos de sus reclamos de Medicare a " +msgstr "¿Desea compartir sus datos de Medicare con " #: templates/design_system/new_authorize_v2.html:58 msgid "If you connect, " -msgstr "Si se conecta, " +msgstr "Si los comparte, " #: templates/design_system/new_authorize_v2.html:58 msgid "will have access to information about your:" @@ -94,7 +95,7 @@ msgstr "Medicamentos recetados" #: templates/design_system/new_authorize_v2.html:70 msgid "You can also grant access to your:" -msgstr "También puede darle acceso a su:" +msgstr "También puede compartir su:" #: templates/design_system/new_authorize_v2.html:89 msgid "Personal Information" @@ -102,15 +103,15 @@ msgstr "Información personal" #: templates/design_system/new_authorize_v2.html:89 msgid "Your name, address, date of birth, race, and gender" -msgstr "Su nombre, dirección, fecha de nacimiento y género" +msgstr "Su nombre, dirección, fecha de nacimiento y sexo" #: templates/design_system/new_authorize_v2.html:92 msgid "Uncheck to block access to personal information." -msgstr "Quite la marca para bloquear el acceso a información personal" +msgstr "Quite la marca para no compartir su información personal." #: templates/design_system/new_authorize_v2.html:93 msgid "Check to allow access to personal information." -msgstr "Marque para permitir el acceso a la información personal." +msgstr "Añade la marca para compartir su información personal." #: templates/design_system/new_authorize_v2.html:127 msgid "Learn more about how " @@ -137,8 +138,8 @@ msgid " will have access to your data " msgstr " tendrá acceso a sus datos " #: templates/design_system/new_authorize_v2.html:145 -msgid "until " -msgstr "hasta el " +msgid "for 13 months, until " +msgstr "durante 13 meses, hasta el " #: templates/design_system/new_authorize_v2.html:145 msgid "for 10 hours" diff --git a/templates/design_system/new_authorize_v2.html b/templates/design_system/new_authorize_v2.html index f14017fdb..a6c11525c 100644 --- a/templates/design_system/new_authorize_v2.html +++ b/templates/design_system/new_authorize_v2.html @@ -142,7 +142,7 @@

{% - + {{ application.name }}{% trans " will have access to your data "%}{%trans permission_end_date_text %}{% if permission_end_date is None %}. {% else %}{{ permission_end_date|localize }}.{% endif %} diff --git a/templates/email/email-activate.html b/templates/email/email-activate.html index 16ec69848..03305c363 100644 --- a/templates/email/email-activate.html +++ b/templates/email/email-activate.html @@ -12,7 +12,6 @@

Welcome to the Blue Button 2.0 Developer Sandbox!

To verify your email address and complete your signup, click the button below (Note, link will expire in {{ EXPIRATION }} days).

-

Activation Key: {{ ACTIVATION_KEY }}

diff --git a/templates/email/email-activate.txt b/templates/email/email-activate.txt index e5bfaee23..ee3cd4f73 100644 --- a/templates/email/email-activate.txt +++ b/templates/email/email-activate.txt @@ -8,7 +8,6 @@ your signup: {{ ACTIVATION_LINK }} -Activation Key: {{ ACTIVATION_KEY }} Note, link will expire in {{ EXPIRATION }} days! {% endblock %} diff --git a/templates/email/email-common-get-started-template.html b/templates/email/email-common-get-started-template.html index 5278ec13b..4233661c6 100644 --- a/templates/email/email-common-get-started-template.html +++ b/templates/email/email-common-get-started-template.html @@ -16,160 +16,8 @@ - - - - - - -
- - - - - - - - -
- -

Get started with Blue Button 2.0

- -
- - - -
- - - - - - -
- - - - - - - -
- - - - - -
-

Read Our Documentation

- -

Check out our documentation to get your app up and running.

- -
-
- - - -
- - - - - - -
- - - - - - - -
- - - - - -
-

Get Help and Support

- -

Join our Google Group to find the support you need.

- -
-
- - - -
- - - - - - -
- - - - - - - -
- - - - - -
-

Get Started with Our Sample Apps

- -

Our Blue Button 2.0 API Resources can help make your idea a reality.

- -
-
- - - -
+ + - + - +