diff --git a/.circleci/build-and-test/jobs.yml b/.circleci/build-and-test/jobs.yml index 4e32831f8..5e58a99ae 100644 --- a/.circleci/build-and-test/jobs.yml +++ b/.circleci/build-and-test/jobs.yml @@ -4,7 +4,7 @@ steps: - checkout - docker-compose-check - - docker-compose-up-backend + - docker-compose-up-with-elastic-backend - run: name: Run Unit Tests And Create Code Coverage Report command: | @@ -47,7 +47,7 @@ steps: - checkout - docker-compose-check - - docker-compose-up-backend + - docker-compose-up-with-elastic-backend - docker-compose-up-frontend - install-nodejs-machine - disable-npm-audit @@ -61,7 +61,7 @@ wait-for-it --service http://web:8080 --timeout 180 -- echo \"Django is ready\"" - run: name: apply the migrations - command: cd tdrs-backend; docker-compose exec web bash -c "python manage.py makemigrations; python manage.py migrate" + command: cd tdrs-backend; docker-compose exec web bash -c "python manage.py makemigrations; python manage.py migrate" - run: name: Remove existing cypress test users command: cd tdrs-backend; docker-compose exec web python manage.py delete_cypress_users -usernames new-cypress@teamraft.com cypress-admin@teamraft.com diff --git a/.circleci/util/commands.yml b/.circleci/util/commands.yml index ebbdfb7e1..09d175b69 100644 --- a/.circleci/util/commands.yml +++ b/.circleci/util/commands.yml @@ -11,6 +11,12 @@ name: Build and spin-up Django API service command: cd tdrs-backend; docker network create external-net; docker-compose up -d --build + docker-compose-up-with-elastic-backend: + steps: + - run: + name: Build and spin-up Django API service + command: cd tdrs-backend; docker network create external-net; docker-compose --profile elastic_setup up -d --build + cf-check: steps: - run: diff --git a/docs/Sprint-Review/sprint-91-summary.md b/docs/Sprint-Review/sprint-91-summary.md new file mode 100644 index 000000000..bfa6372a2 --- /dev/null +++ b/docs/Sprint-Review/sprint-91-summary.md @@ -0,0 +1,62 @@ +# Sprint 91 Summary + +01/17/2024 - 01/30/2024 + +Velocity (Dev): 24 + +## Sprint Goal +* Dev: + * Continue parsing engine development and begin work on enhancement tickets + * #2536 Cat 4 validation + * #1858 Secure OFA staff access to Kibana + * Unblocks #1350 when complete +* DevOps: + * #2790 - Update deployment code to support Kibana and integrate with Standing Elastic instance +* Design: + * Tie up current documentation work + * Continue refinement of research roadmap + + +## Tickets +### Completed/Merged +* [#2751 Resource Card updated with latest coding instructions](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2751) + +### Ready to Merge +* [#2772 Elastic bulk document creation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2772) +* [#1350 Kibana access from TDP](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/1350) +* [#1858 Spike: Secure Kibana access](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/1858) +* [#2711 Catch report month / year mismatches](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2711) + + + + +### Submitted (QASP Review, OCIO Review) +* [#2790 Kibana Deployment](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2790) +* [#2681 Section 1 Validation clean-up](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2681) + + + +### Closed (not merged) +* N/A + + +--- + +## Moved to Next Sprint (In Progress, Blocked, Raft Review) +### In Progress +* [#2646 - Populate data file summary case aggregates differently per section](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2646) +* [#2820 [bug] Uncaught exception re: parsing error preventing feedback report generation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2820) +* [#2768 Fix production OWASP scan reporting](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2768) +* [#2799 Generate error mismatching field rpt_month_year w/ header](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2799) +* [#2781 As a developer, I want to have documentation on django migration best practices](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2781) + + +### Blocked +* N/A + +### Raft Review +* [#2536 [spike] Cat 4 validation](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2536) +* [#2592 Deploy celery as a separate cloud.gov app](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2592) +* [#2746 As an STT, I need to know if there are issues with the DOBs reported in my data files](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2746) +* [#2813 Reduce dev environment count](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2813) +* [#2729 As a developer, I want to move migration commands in the pipeline to CircleCI](https://app.zenhub.com/workspaces/sprint-board-5f18ab06dfd91c000f7e682e/issues/gh/raft-tech/tanf-app/2729) diff --git a/docs/Technical-Documentation/README.md b/docs/Technical-Documentation/README.md index 31d6a3214..e6ef1b203 100644 --- a/docs/Technical-Documentation/README.md +++ b/docs/Technical-Documentation/README.md @@ -19,7 +19,7 @@ This directory contains system and architecture documentation including diagrams - [jwt-key-rotation.md](./jwt-key-rotation.md) : Describes the process for rotating JWT keys in Login.gov. - [nexus-repo.md](./nexus-repo.md) : Setup, connection information, and how to use our Nexus Artifact Repository - [openid-connect.md](./openid-connect.md) : Provides an architecture-level view of the OpenID Connect prototocol. -- [rafts-accessibility-dos-and-donts.md](./rafts-accessibility-dos-and-donts.md) : A succint list of UX guidelines for frontend accessibility. +- [accessibility-guide.md](./accessibility-guide.md) : A guide on getting started with accessibility testing tools and TDP-relevant resources. - [remote-development.md](./remote-development.md) : A guide on doing live remote development in Cloud.gov. - [unit-tests.md](./unit-tests.md) : Outlines our unit testing frameworks and how to run these manually. - [user_role_management.md](./user_role_management.md) : Provides an overview of our user management in Django Administrator Console. diff --git a/docs/Technical-Documentation/accessibility-guide.md b/docs/Technical-Documentation/accessibility-guide.md new file mode 100644 index 000000000..ae938ef1f --- /dev/null +++ b/docs/Technical-Documentation/accessibility-guide.md @@ -0,0 +1,236 @@ +# Accessibility Guide + +**Table of Contents:** +- [Background](#Background) +- [Relevant standards](#Relevant-standards) +- [State of a11y in TDP](#State-of-a11y-in-TDP) +- [What to keep in mind when testing](#What-to-keep-in-mind-when-testing) +- [Testing tools](#Testing-tools) +- [Screen reader use and setup](#Screen-reader-use-and-setup) +- [Do's and don'ts when designing](#Dos-and-donts-when-designing) +- [References](#References) + +--- + +## Background +This document has evolved from its initial state of "Helpful a11y stuff" to one intended to serve more as a guided tour of Raft's accessibility (a11y) practice aimed at enabling more testing and broadly more *consideration* of a11y to be shifted left. + +Additionally, this resource will also aim to document the current state of a11y in TDP to track outstanding enhancements or a11y fixes and to help commentate some issues that may be encountered when testing TDP pages for accessibility conformance. While TDP remains highly accessible as a whole there are certain issues we've identified that demand follow-on work to research or correct. There are also a number of false positives that certain a11y testing tools can identify as problematic. + +--- + +## Relevant standards +There are numerous areas of accessibility law that apply to our work ranging from the ADA (Americans with Disabilities Act) which lays out the broad requirement of accessibility in public spaces and systems to Section 508 of the Rehabilitation Act which mandates a specific standard that Federal systems & resources need to adhere to—specifically WCAG (Web Content Accessibility Standards) 2.0 AA. [See more on US accessibility law](https://www.ada.gov/resources/disability-rights-guide/#top). + +WCAG 2.x standards have four categories with which to evaluate accessibility; Perceivable, Operable, Understandable, and Robust. Within all four categories are three levels of conformance, A, AA, AAA; respectively these correspond to the most barebones standards, good baseline standards, and the most specific standards. + +--- + +## State of a11y in TDP +The [errors audit](https://hackmd.io/79rAOVzISbOvaTNv8nSpeA) tracks all outstanding accessibility issues in the TANF Data Portal, its knowledge center, and Django Admin console. + +--- + +## What to keep in mind when testing +The full picture of what makes for *complete* accessibility testing involves every check in Accessibility Insight's Full Assessment tool, thoughtfulness around what makes for a good experience through the lens of various assistive technologies, and real-world usability testing with people experiencing disabilities. This guide isn't going to be able to deliver all that—but it will seek to lay out some illustrative examples and frequently useful questions to pose of an experience when you're testing it for conformance. + +The following checklist is organized via WCAG 2.x's categories. While a few items explicitly involve screen-readers or other assistive technologies, most items should be able to be checked "yes" regardless of your mode of interaction with the webpage (vision & mouse, keyboard only, screenreader, etc...). + + +### Is it Perceivable? +The user must be able to *perceive* all the information being presented. + +- [ ] If there are non-decorative images on the page, do they have alt-text? + - [ ] Does the alt text convey all the relevant information a sighted user would get from the image? +- [ ] Is there a visible focus indicator for every element of the interface as you tab through it? +- [ ] Do all interface items have sufficient contrast against their backgrounds? + - [ ] If information is communicated by color are there also alternatives beyond color for folks who can't see or distinguish the colors? +- [ ] Do related areas of the interface have visual proximity to each other? +- [ ] Are all interface items read correctly by screenreaders? + +### Is it Operable? +The user must be able to *use* all interactive portions of the experience—and navigate to all areas of it. + +- [ ] When navigating the page with the keyboard can you tab to every interactive element? + - [ ] Is the order in which items are focused logical? + - [ ] If there's a disabled element is it correctly marked up with ARIA and read to screen readers? +- [ ] Is there sufficient (read as: generous) time to read and interact with transient elements of the page (e.g. the timeout modal dialog)? +- [ ] Does the navigable experience "shrink" when pop-over content is open? (e.g. Modal dialogs & the opened side navigation) + - [ ] Is content behind the pop-over content shaded out? + - [ ] Is keyboard focus constrained to the pop-over content alone? + +### Is it Understandable? +The user must be able to understand the information being presented by the experience and *how* to operate it. + +- [ ] Is the language used on the page [plain](https://www.plainlanguage.gov/guidelines/) rather than technical or overcomplicated? + - [ ] Are abbreviations defined alongside their first usage? + - [ ] Are unusual words explained? +- [ ] Do interface elements used for navigation appear and behave consistently throughout the experience? +- [ ] Does the experience make it clear when elements or page contexts change? (e.g. When you navigate to a new page or a new piece of content appears) +- [ ] Are there labels and instructions on the page to help prevent errors? + - [ ] When errors appear is it clear what action or form item they relate to? + - [ ] When errors appear do they suggest something for the user to try next to correct or move past them? + +### Is it Robust? +The experience and the content within it need to be *reliable* and play nicely with a range of assistive technologies. + +- [ ] Does this experience work as intended when navigated via Safari and Voiceover? + - [ ] Does it work as intended when tested in other browsers and via other screen readers? +- [ ] Is the page parsed correctly by testing tools? + + +--- + +## Testing tools + + + + +### In-browser tools + +| Extension | Description | Link | +| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **Alt-text tester** | Flags images on a page that are missing alt text, provides an easy way to view alt text for any image that has it. | [Chrome](https://chrome.google.com/webstore/detail/alt-text-tester/koldhcllpbdfcdpfpbldbicbgddglodk?hl=en) | +| **Accessibility Insights** | Great for getting familiar with WCAG. It has both fast-pass assessments and a guided way to do manual testing. Plus—when doing manual testing—deep dive explainers on each test (see the info buttons next to the headings of any given test). | [Chrome](https://chrome.google.com/webstore/detail/accessibility-insights-fo/pbjjkligggfmakdaogkfomddhfmpjeni), [Edge](https://microsoftedge.microsoft.com/addons/detail/ghbhpcookfemncgoinjblecnilppimih) | +| **Axe DevTools** | Great fast-pass scanner, will identify best practice issues as well as WCAG compliance violations. | [Chrome](https://chrome.google.com/webstore/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Edge](https://microsoftedge.microsoft.com/addons/detail/axe-devtools-web-access/kcenlimkmjjkdfcaleembgmldmnnlfkn) | +| **Accessible Colors** | Web tool tool that will tell you whether two hex codes have sufficient contrast with each other, show you what they look like, and suggests the closest alternative color if they don't have sufficient contrast. Note that the default values for font size, weight, and compliance can be left alone for most purposes. | [Site](https://accessible-colors.com/) | + + +If you go through all the manual tests in the full Accessibility Insights assessment having these scripts bookmarked will be useful! Running them is as simple as opening the bookmark while viewing the page you're testing. + +**Tests whether page text can be spaced out and comply with requirements without breaking layout** + +```` +javascript:(function(){ var style = document.createElement(%27style%27), styleContent = document.createTextNode(%27* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; } p{ margin-bottom: 2em !important; } %27); style.appendChild(styleContent ); document.getElementsByTagName(%27head%27)[0].appendChild(style); var iframes = document.querySelectorAll(%27iframe%27);for (var i=0; i";break;case Node.COMMENT_NODE:b+="<\!--"+a.nodeValue+"--\>";break;case Node.DOCUMENT_TYPE_NODE:b+="\n"}a=a.nextSibling}return b}(document),d=document.createElement("form");d.method="POST";d.action="https://validator.w3.org/nu/";d.enctype="multipart/form-data";d.target="_blank";d.acceptCharset="utf-8";c("showsource","yes");c("content",e);document.body.appendChild(d);d.submit()})(); +```` + +**Filters results of the above to only WCAG 2.0 violations** + +```` +javascript:(function(){var removeNg=true;var filterStrings=["tag seen","Stray end tag","Bad start tag","violates nesting rules","Duplicate ID","first occurrence of ID","Unclosed element","not allowed as child of element","unclosed elements","not allowed on element","unquoted attribute value","Duplicate attribute"];var filterRE,root,results,result,resultText,i,cnt=0;filterRE=filterStrings.join("|");root=document.getElementById("results");if(!root){alert("No results container found.");return}results=root.getElementsByTagName("li");for(i=0;i> "$BASH_ENV" fi - -exit $ZAP_EXIT diff --git a/tdrs-backend/docker-compose.yml b/tdrs-backend/docker-compose.yml index a6624688b..6a09c3944 100644 --- a/tdrs-backend/docker-compose.yml +++ b/tdrs-backend/docker-compose.yml @@ -50,7 +50,7 @@ services: ports: - 5601:5601 environment: - - xpack.security.encryptionKey="something_at_least_32_characters" + - xpack.security.encryptionKey=${KIBANA_ENCRYPTION_KEY:-something_at_least_32_characters} - xpack.security.session.idleTimeout="1h" - xpack.security.session.lifespan="30d" volumes: @@ -58,12 +58,42 @@ services: depends_on: - elastic + # This task only needs to be performed once, during the *initial* startup of + # the stack. Any subsequent run will reset the passwords of existing users to + # the values defined inside the '.env' file, and the built-in roles to their + # default permissions. + # + # By default, it is excluded from the services started by 'docker compose up' + # due to the non-default profile it belongs to. To run it, either provide the + # '--profile=elastic_setup' CLI flag to Compose commands, or "up" the service by name + # such as 'docker compose up elastic_setup'. + elastic_setup: + profiles: + - elastic_setup + build: + context: elastic_setup/ + args: + ELASTIC_VERSION: "7.17.6" + init: true + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD:-changeme} + KIBANA_SYSTEM_PASSWORD: ${KIBANA_SYSTEM_PASSWORD:-changeme} + OFA_ADMIN_PASSWORD: ${OFA_ADMIN_PASSWORD:-changeme} + ELASTICSEARCH_HOST: ${ELASTICSEARCH_HOST:-elastic} + depends_on: + - elastic + elastic: image: elasticsearch:7.17.6 environment: - discovery.type=single-node - logger.discovery.level=debug - - xpack.security.enabled=false + - xpack.security.enabled=true + - xpack.security.authc.anonymous.username="ofa_admin" + - xpack.security.authc.anonymous.roles="ofa_admin" + - xpack.security.authc.anonymous.authz_exception=true + - ELASTIC_PASSWORD=${ELASTIC_PASSWORD:-changeme} + - KIBANA_SYSTEM_PASSWORD=${KIBANA_SYSTEM_PASSWORD:-changeme} ports: - 9200:9200 - 9300:9300 @@ -101,6 +131,7 @@ services: - CYPRESS_TOKEN - DJANGO_DEBUG - SENDGRID_API_KEY + - BYPASS_KIBANA_AUTH volumes: - .:/tdpapp image: tdp diff --git a/tdrs-backend/elastic_setup/Dockerfile b/tdrs-backend/elastic_setup/Dockerfile new file mode 100644 index 000000000..32e6429f6 --- /dev/null +++ b/tdrs-backend/elastic_setup/Dockerfile @@ -0,0 +1,10 @@ +ARG ELASTIC_VERSION + +FROM docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION} + +COPY . / + +RUN ["chmod", "+x", "/entrypoint.sh"] +RUN ["chmod", "+x", "/util.sh"] + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/tdrs-backend/elastic_setup/entrypoint.sh b/tdrs-backend/elastic_setup/entrypoint.sh new file mode 100644 index 000000000..6073b0540 --- /dev/null +++ b/tdrs-backend/elastic_setup/entrypoint.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +source "${BASH_SOURCE[0]%/*}"/util.sh + + +# -------------------------------------------------------- +# Users declarations + +declare -A users_passwords +users_passwords=( + [kibana_system]="${KIBANA_SYSTEM_PASSWORD:-}" + [ofa_admin]="${OFA_ADMIN_PASSWORD:-}" +) + +declare -A users_roles +users_roles=( + [kibana_system]='kibana_system' + [ofa_admin]='kibana_admin' +) + +# -------------------------------------------------------- +# Roles declarations for custom roles + +declare -A roles_files +roles_files=( + +) + +# -------------------------------------------------------- + + +log 'Waiting for availability of Elasticsearch. This can take several minutes.' + +declare -i exit_code=0 +wait_for_elasticsearch || exit_code=$? + +if ((exit_code)); then + case $exit_code in + 6) + suberr 'Could not resolve host. Is Elasticsearch running?' + ;; + 7) + suberr 'Failed to connect to host. Is Elasticsearch healthy?' + ;; + 28) + suberr 'Timeout connecting to host. Is Elasticsearch healthy?' + ;; + *) + suberr "Connection to Elasticsearch failed. Exit code: ${exit_code}" + ;; + esac + + exit $exit_code +fi + +sublog 'Elasticsearch is running' + +log 'Waiting for initialization of built-in users' + +wait_for_builtin_users || exit_code=$? + +if ((exit_code)); then + suberr 'Timed out waiting for condition' + exit $exit_code +fi + +sublog 'Built-in users were initialized' + +for role in "${!roles_files[@]}"; do + log "Role '$role'" + + declare body_file + body_file="${BASH_SOURCE[0]%/*}/roles/${roles_files[$role]:-}" + if [[ ! -f "${body_file:-}" ]]; then + sublog "No role body found at '${body_file}', skipping" + continue + fi + + sublog 'Creating/updating' + ensure_role "$role" "$(<"${body_file}")" +done + +for user in "${!users_passwords[@]}"; do + log "User '$user'" + if [[ -z "${users_passwords[$user]:-}" ]]; then + sublog 'No password defined, skipping' + continue + fi + + declare -i user_exists=0 + user_exists="$(check_user_exists "$user")" + + if ((user_exists)); then + sublog 'User exists, setting password' + set_user_password "$user" "${users_passwords[$user]}" + else + if [[ -z "${users_roles[$user]:-}" ]]; then + suberr ' No role defined, skipping creation' + continue + fi + + sublog 'User does not exist, creating' + create_user "$user" "${users_passwords[$user]}" "${users_roles[$user]}" + fi +done + +log "Elastic setup completed. Exiting with code: $?" diff --git a/tdrs-backend/elastic_setup/util.sh b/tdrs-backend/elastic_setup/util.sh new file mode 100644 index 000000000..045110249 --- /dev/null +++ b/tdrs-backend/elastic_setup/util.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash + +# Log a message. +function log { + echo "[+] $1" +} + +# Log a message at a sub-level. +function sublog { + echo " ⠿ $1" +} + +# Log an error. +function err { + echo "[x] $1" >&2 +} + +# Log an error at a sub-level. +function suberr { + echo " ⠍ $1" >&2 +} + +# Poll the 'elasticsearch' service until it responds with HTTP code 200. +function wait_for_elasticsearch { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' "http://${elasticsearch_host}:9200/" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + # retry for max 300s (60*5s) + for _ in $(seq 1 60); do + local -i exit_code=0 + output="$(curl "${args[@]}")" || exit_code=$? + + if ((exit_code)); then + result=$exit_code + fi + + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + break + fi + + sleep 5 + done + + if ((result)) && [[ "${output: -3}" -ne 000 ]]; then + echo -e "\n${output::-3}" + fi + + return $result +} + +# Poll the Elasticsearch users API until it returns users. +function wait_for_builtin_users { + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' "http://${elasticsearch_host}:9200/_security/user?pretty" ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + + local line + local -i exit_code + local -i num_users + + # retry for max 30s (30*1s) + for _ in $(seq 1 30); do + num_users=0 + + # read exits with a non-zero code if the last read input doesn't end + # with a newline character. The printf without newline that follows the + # curl command ensures that the final input not only contains curl's + # exit code, but causes read to fail so we can capture the return value. + # Ref. https://unix.stackexchange.com/a/176703/152409 + while IFS= read -r line || ! exit_code="$line"; do + if [[ "$line" =~ _reserved.+true ]]; then + (( num_users++ )) + fi + done < <(curl "${args[@]}"; printf '%s' "$?") + + if ((exit_code)); then + result=$exit_code + fi + + # we expect more than just the 'elastic' user in the result + if (( num_users > 1 )); then + result=0 + break + fi + + sleep 1 + done + + return $result +} + +# Verify that the given Elasticsearch user exists. +function check_user_exists { + local username=$1 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local -i exists=0 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 || "${output: -3}" -eq 404 ]]; then + result=0 + fi + if [[ "${output: -3}" -eq 200 ]]; then + exists=1 + fi + + if ((result)); then + echo -e "\n${output::-3}" + else + echo "$exists" + fi + + return $result +} + +# Set password of a given Elasticsearch user. +function set_user_password { + local username=$1 + local password=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}/_password" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\" : \"${password}\"}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Create the given Elasticsearch user. +function create_user { + local username=$1 + local password=$2 + local role=$3 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/user/${username}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "{\"password\":\"${password}\",\"roles\":[\"${role}\"]}" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} + +# Ensure that the given Elasticsearch role is up-to-date, create it if required. +function ensure_role { + local name=$1 + local body=$2 + + local elasticsearch_host="${ELASTICSEARCH_HOST:-elastic}" + + local -a args=( '-s' '-D-' '-m15' '-w' '%{http_code}' + "http://${elasticsearch_host}:9200/_security/role/${name}" + '-X' 'POST' + '-H' 'Content-Type: application/json' + '-d' "$body" + ) + + if [[ -n "${ELASTIC_PASSWORD:-}" ]]; then + args+=( '-u' "elastic:${ELASTIC_PASSWORD}" ) + fi + + local -i result=1 + local output + + output="$(curl "${args[@]}")" + if [[ "${output: -3}" -eq 200 ]]; then + result=0 + fi + + if ((result)); then + echo -e "\n${output::-3}\n" + fi + + return $result +} \ No newline at end of file diff --git a/tdrs-backend/kibana.yml b/tdrs-backend/kibana.yml index dad4335d0..e98d2438d 100644 --- a/tdrs-backend/kibana.yml +++ b/tdrs-backend/kibana.yml @@ -1,2 +1,12 @@ elasticsearch.hosts: ["http://elastic:9200"] server.host: kibana +elasticsearch.username: kibana_system +elasticsearch.password: changeme +xpack.security.authc.providers: + anonymous.anonymous1: + order: 0 + description: "OFA Admin Login" + hint: "" + credentials: + username: "ofa_admin" + password: "changeme" diff --git a/tdrs-backend/tdpservice/settings/common.py b/tdrs-backend/tdpservice/settings/common.py index dc4e4c51e..108586c80 100644 --- a/tdrs-backend/tdpservice/settings/common.py +++ b/tdrs-backend/tdpservice/settings/common.py @@ -465,11 +465,14 @@ class Common(Configuration): } } - # Elastic + # Elastic/Kibana ELASTICSEARCH_DSL = { 'default': { 'hosts': os.getenv('ELASTIC_HOST', 'elastic:9200'), + 'http_auth': ('elastic', os.getenv('ELASTIC_PASSWORD', 'changeme')) }, } + KIBANA_BASE_URL = os.getenv('KIBANA_BASE_URL', 'http://localhost:5601') + BYPASS_KIBANA_AUTH = strtobool(os.getenv("BYPASS_KIBANA_AUTH", "no")) CYPRESS_TOKEN = os.getenv('CYPRESS_TOKEN', None) diff --git a/tdrs-backend/tdpservice/urls.py b/tdrs-backend/tdpservice/urls.py index 26858b356..368314c92 100755 --- a/tdrs-backend/tdpservice/urls.py +++ b/tdrs-backend/tdpservice/urls.py @@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny -from .users.api.authorization_check import AuthorizationCheck +from .users.api.authorization_check import AuthorizationCheck, KibanaAuthorizationCheck from .users.api.login import TokenAuthorizationLoginDotGov, TokenAuthorizationAMS from .users.api.login import CypressLoginDotGovAuthenticationOverride from .users.api.login_redirect_oidc import LoginRedirectAMS, LoginRedirectLoginDotGov @@ -52,6 +52,7 @@ urlpatterns = [ path("v1/", include(urlpatterns)), path("admin/", admin.site.urls, name="admin"), + path("kibana/", KibanaAuthorizationCheck.as_view(), name="kibana-authorization-check"), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # TODO: Supply `terms_of_service` argument in OpenAPI Info once implemented diff --git a/tdrs-backend/tdpservice/users/api/authorization_check.py b/tdrs-backend/tdpservice/users/api/authorization_check.py index 3ac867be0..76afeecb1 100644 --- a/tdrs-backend/tdpservice/users/api/authorization_check.py +++ b/tdrs-backend/tdpservice/users/api/authorization_check.py @@ -4,10 +4,12 @@ from django.contrib.auth import logout from django.middleware import csrf from django.utils import timezone -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from ..serializers import UserProfileSerializer +from django.http import HttpResponseRedirect +from django.conf import settings logger = logging.getLogger(__name__) @@ -49,3 +51,21 @@ def get(self, request, *args, **kwargs): else: logger.info("Auth check FAIL for user on %s", timezone.now()) return Response({"authenticated": False}) + +class KibanaAuthorizationCheck(APIView): + """Check if user is authorized to view Kibana.""" + + query_string = False + pattern_name = "kibana-authorization-check" + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + """Handle get request and verify user is authorized to access kibana.""" + user = request.user + + user_in_valid_group = user.is_ofa_sys_admin + + if (user.hhs_id is not None and user_in_valid_group) or settings.BYPASS_KIBANA_AUTH: + return HttpResponseRedirect(settings.KIBANA_BASE_URL) + else: + return HttpResponseRedirect(settings.FRONTEND_BASE_URL) diff --git a/tdrs-backend/tdpservice/users/models.py b/tdrs-backend/tdpservice/users/models.py index d0a9c924d..2dd8dd3c1 100644 --- a/tdrs-backend/tdpservice/users/models.py +++ b/tdrs-backend/tdpservice/users/models.py @@ -180,6 +180,11 @@ def is_ocio_staff(self) -> bool: """Return whether or not the user is in the ACF OCIO Group.""" return self.is_in_group("ACF OCIO") + @property + def is_ofa_sys_admin(self) -> bool: + """Return whether or not the user is in the OFA System Admin Group.""" + return self.is_in_group("OFA System Admin") + @property def is_deactivated(self): """Check if the user's account status has been set to 'Deactivated'.""" diff --git a/tdrs-frontend/nginx/local/locations.conf b/tdrs-frontend/nginx/local/locations.conf index 2fc38d3ad..154cda557 100644 --- a/tdrs-frontend/nginx/local/locations.conf +++ b/tdrs-frontend/nginx/local/locations.conf @@ -4,7 +4,7 @@ location = /nginx_status { deny all; } -location ~ ^/(v1|admin|static/admin|swagger|redocs) { +location ~ ^/(v1|admin|static/admin|swagger|redocs|kibana) { limit_req zone=limitreqsbyaddr delay=5; proxy_pass http://${BACK_END}:8080$request_uri; proxy_set_header Host $host:3000; diff --git a/tdrs-frontend/src/components/Header/Header.jsx b/tdrs-frontend/src/components/Header/Header.jsx index 2f6c5335b..201cd55bf 100644 --- a/tdrs-frontend/src/components/Header/Header.jsx +++ b/tdrs-frontend/src/components/Header/Header.jsx @@ -7,6 +7,7 @@ import { accountStatusIsApproved, accountIsInReview, accountCanViewAdmin, + accountCanViewKibana, } from '../../selectors/auth' import NavItem from '../NavItem/NavItem' @@ -29,6 +30,7 @@ function Header() { const userAccessRequestPending = useSelector(accountIsInReview) const userAccessRequestApproved = useSelector(accountStatusIsApproved) const userIsAdmin = useSelector(accountCanViewAdmin) + const userIsSysAdmin = useSelector(accountCanViewKibana) const menuRef = useRef() @@ -137,6 +139,13 @@ function Header() { href={`${process.env.REACT_APP_BACKEND_HOST}/admin/`} /> )} + {userIsSysAdmin && ( + + )} )} diff --git a/tdrs-frontend/src/components/ResourceCards/ResourceCards.jsx b/tdrs-frontend/src/components/ResourceCards/ResourceCards.jsx index a3285dbb3..96b312603 100644 --- a/tdrs-frontend/src/components/ResourceCards/ResourceCards.jsx +++ b/tdrs-frontend/src/components/ResourceCards/ResourceCards.jsx @@ -70,7 +70,7 @@ function ResourceCards() { buttonId="viewACFFormInstructions" title="ACF-199 and ACF-209 Instructions" body="Instructions and definitions for completion of forms ACF-199 (TANF Data Report) and ACF-209 (SSP-MOE Data Report)." - link="https://www.acf.hhs.gov/sites/default/files/documents/ofa/tanf_data_reports_tan_ssp_instructions_definitions.pdf" + link="https://www.acf.hhs.gov/ofa/policy-guidance/acf-ofa-pi-23-04" linkText="View ACF Form Instructions" /> diff --git a/tdrs-frontend/src/components/ResourceCards/ResourceCards.test.js b/tdrs-frontend/src/components/ResourceCards/ResourceCards.test.js index 8c833b555..1ce5da893 100644 --- a/tdrs-frontend/src/components/ResourceCards/ResourceCards.test.js +++ b/tdrs-frontend/src/components/ResourceCards/ResourceCards.test.js @@ -83,8 +83,7 @@ describe('ResourceCards', () => { it('redirects to ACF Form Instructions when View ACF Form Instructions clicked', () => { const store = mockStore(initialState) - const url = - 'https://www.acf.hhs.gov/sites/default/files/documents/ofa/tanf_data_reports_tan_ssp_instructions_definitions.pdf' + const url = 'https://www.acf.hhs.gov/ofa/policy-guidance/acf-ofa-pi-23-04' const wrapper = mount( diff --git a/tdrs-frontend/src/components/SiteMap/SiteMap.jsx b/tdrs-frontend/src/components/SiteMap/SiteMap.jsx index 1df805e7d..5ad40fc4e 100644 --- a/tdrs-frontend/src/components/SiteMap/SiteMap.jsx +++ b/tdrs-frontend/src/components/SiteMap/SiteMap.jsx @@ -3,11 +3,13 @@ import { useSelector } from 'react-redux' import { accountStatusIsApproved, accountCanViewAdmin, + accountCanViewKibana, } from '../../selectors/auth' const SiteMap = ({ user }) => { const userIsApproved = useSelector(accountStatusIsApproved) const userIsAdmin = useSelector(accountCanViewAdmin) + const userIsSysAdmin = useSelector(accountCanViewKibana) return (
@@ -31,6 +33,13 @@ const SiteMap = ({ user }) => { link={`${process.env.REACT_APP_BACKEND_HOST}/admin/`} /> )} + + {userIsSysAdmin && ( + + )}
) } diff --git a/tdrs-frontend/src/selectors/auth.js b/tdrs-frontend/src/selectors/auth.js index b79d2b6b1..ab962e275 100644 --- a/tdrs-frontend/src/selectors/auth.js +++ b/tdrs-frontend/src/selectors/auth.js @@ -59,3 +59,7 @@ export const accountCanViewAdmin = (state) => ['Developer', 'OFA System Admin', 'ACF OCIO', 'OFA Admin'].includes( selectPrimaryUserRole(state)?.name ) + +export const accountCanViewKibana = (state) => + accountStatusIsApproved(state) && + ['Developer', 'OFA System Admin'].includes(selectPrimaryUserRole(state)?.name)