Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Eregcsc 2012 initial logins from eua #991

Merged
merged 20 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,31 @@ To better support Rapid Prototyping, a VueJS Single Page Application (SPA) has b
3. edit files in `/regulations/static/prototype` to make changes
4. changes should be reflected in running prototype via hot reloading
5. `make prototype:clean` to tear down Docker container

## Setting local to use EUA
1. Update your Dockerfile with the following environment variables
```
ENV OIDC_RP_CLIENT_ID=<your client id>
ENV OIDC_RP_CLIENT_SECRET=<your client secret>
ENV OIDC_OP_AUTHORIZATION_ENDPOINT=<authorization endpoint>
ENV OIDC_OP_TOKEN_ENDPOINT=<token endpoint>
ENV OIDC_OP_USER_ENDPOINT=<user endpoint>
ENV OIDC_OP_JWKS_ENDPOINT=<jwks endpoint>
ENV EUA_FEATUREFLAG=<set to 'true' if you want to see the eua link on admin login page>
```
These values can be found on AWS Parameter store.

## Register to test idp idm
- Sign into the URL [https://test.idp.idm.cms.gov/](https://test.idp.idm.cms.gov/) to access the CMS IDP (Identity Provider) portal.
- Set up Multi-Factor Authentication (MFA) for your account. Follow the provided prompts and instructions to complete the MFA setup process.
- Once your account has been successfully set up with MFA, please notify the CMS Okta team.
- Inform the CMS Okta team that you need to be added to the eRegs group.

Please note that the provided URL (https://test.idp.idm.cms.gov/) may require a valid CMS IDP account to access.

## Trouble shooting tips
- Issue: Setting OIDC_OP_AUTHORIZATION_ENDPOINT not found
This error indicates that the environment variables are not properly set.
- Solution:
- On your local environment verify that the DJANGO_SETTINGS_MODULE environment variable is set to ${DJANGO_SETTINGS_MODULE:-cmcs_regulations.settings.euasettings}. You can modify your docker-compose.yml file to include this setting: DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-cmcs_regulations.settings.euasettings}.
- On dev,val,prod ensure that DJANGO_SETTINGS_MODULE is set correctly in AWS Param Store.
5 changes: 3 additions & 2 deletions solution/backend/cmcs_regulations/settings/euasettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
OIDC_RP_IDP_SIGN_KEY = os.environ.get("OIDC_RP_IDP_SIGN_KEY", None)

AUTHENTICATION_BACKENDS = (
'regulations.admin.OidcAdminAuthenticationBackend',
'mozilla_django_oidc.auth.OIDCAuthenticationBackend',
'django.contrib.auth.backends.ModelBackend',
'regulations.admin.OidcAdminAuthenticationBackend',
)

STAGE_ENV = os.environ.get("STAGE_ENV", "")
Expand All @@ -22,6 +22,7 @@
OIDC_OP_TOKEN_ENDPOINT = os.environ.get("OIDC_OP_TOKEN_ENDPOINT", None)
OIDC_OP_USER_ENDPOINT = os.environ.get("OIDC_OP_USER_ENDPOINT", None)
OIDC_OP_JWKS_ENDPOINT = os.environ.get("OIDC_OP_JWKS_ENDPOINT", None)
OIDC_REDIRECT_URL = "/admin/oidc/callback/"
OIDC_RP_SIGN_ALGO = 'RS256'
LOGIN_REDIRECT_URL = '/admin/'
LOGOUT_REDIRECT_URL = '/'
Expand All @@ -30,6 +31,6 @@
if re.match(r'^dev\d*$', STAGE_ENV):
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
elif STAGE_ENV == 'dev' or 'val':
elif STAGE_ENV == 'dev' or STAGE_ENV == 'val':
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
25 changes: 25 additions & 0 deletions solution/backend/cmcs_regulations/settings/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .base import * # noqa
import re
import os

USE_AWS_TOKEN = True
AWS_ACCESS_KEY_ID = os.environ.get("FILE_MANAGER_AWS_ACCESS_KEY_ID", 'test')
AWS_SECRET_ACCESS_KEY = os.environ.get("FILE_MANAGER_AWS_SECRET_ACCESS_KEY", 'test')
Expand All @@ -12,6 +14,29 @@
MEDIA_URL = "https://%s/" % AWS_S3_CUSTOM_DOMAIN
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

# TODO - this should be removed after we merge euasettings.py with base.py in teh future

STAGE_ENV = os.environ.get("STAGE_ENV", "")
BASE_URL = os.environ.get("BASE_URL", "")
OIDC_RP_CLIENT_ID = os.environ.get("OIDC_RP_CLIENT_ID", None)
OIDC_RP_CLIENT_SECRET = os.environ.get("OIDC_RP_CLIENT_SECRET", None)
OIDC_OP_AUTHORIZATION_ENDPOINT = os.environ.get("OIDC_OP_AUTHORIZATION_ENDPOINT", None)
OIDC_OP_TOKEN_ENDPOINT = os.environ.get("OIDC_OP_TOKEN_ENDPOINT", None)
OIDC_OP_USER_ENDPOINT = os.environ.get("OIDC_OP_USER_ENDPOINT", None)
OIDC_OP_JWKS_ENDPOINT = "/example/jwks/endpoint/"
OIDC_REDIRECT_URL = "/admin/oidc/callback/"
OIDC_RP_SIGN_ALGO = 'RS256'
LOGIN_REDIRECT_URL = '/admin/'
LOGOUT_REDIRECT_URL = '/'
EUA_FEATUREFLAG = bool(os.getenv('EUA_FEATUREFLAG', 'False').lower() == 'true')

if re.match(r'^dev\d*$', STAGE_ENV):
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"
elif STAGE_ENV == 'dev' or STAGE_ENV == 'val':
LOGIN_REDIRECT_URL = f"/{STAGE_ENV}/admin/"
LOGOUT_REDIRECT_URL = f"/{STAGE_ENV}/"

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
Expand Down
58 changes: 31 additions & 27 deletions solution/backend/regulations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,42 +70,46 @@ def roman_to_int(roman):
return result


class OktaClaim:
def __init__(self, email, name, preferred_username, jobcodes):
self.email = email
self.name = name
self.preferred_username = preferred_username
self.jobcodes = jobcodes


class OidcAdminAuthenticationBackend(OIDCAuthenticationBackend):
def verify_claims(self, claims: OktaClaim) -> bool:
def verify_claims(self, claims) -> bool:
return (
super().verify_claims(claims)
and claims.get("email_verified", False)
)

@transaction.atomic
def create_user(self, claims: OktaClaim) -> User:
print(f"Creating user {claims.get('email')}")
print(f"Jobcodes: {claims.get('jobcodes')}")
print(f"Name: {claims.get('name')}")
print(f"Preferred username: {claims.get('preferred_username')}")
user: User = self.UserModel.objects.create_user(
claims.get("email"),
None, # password
first_name=claims["given_name"],
last_name=claims["family_name"],
)
user.save()

return user
def create_user(self, claims) -> User:
if claims.get("jobcodes"):
with transaction.atomic():
try:
# Attempt to get the user by email
user = self.UserModel.objects.get(email=claims.get("email"))
except User.DoesNotExist:
# User does not exist, create a new one
user = self.UserModel(
email=claims.get("email"),
username=claims.get("email")
)

# Set user fields from claims
return self.update_user(user, claims)
return None

@transaction.atomic
def update_user(self, user: User, claims: OktaClaim) -> User:
def update_user(self, user: User, claims) -> User:
"""Update existing user with new claims, if necessary save, and return user"""
user.first_name = claims["given_name"]
user.last_name = claims["family_name"]
first_name = claims.get("firstName")
if first_name:
user.first_name = first_name

last_name = claims.get("lastName")
if last_name:
user.last_name = last_name

jobcodes = claims.get("jobcodes")
if jobcodes:
user.is_active = True
else:
user.is_active = False
user.save()

return user
Expand Down
47 changes: 47 additions & 0 deletions solution/backend/regulations/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import unittest
from unittest.mock import patch

from django.contrib.auth import get_user_model

from ..admin import OidcAdminAuthenticationBackend

User = get_user_model()


class OidcAdminAuthenticationBackendTest(unittest.TestCase):
def setUp(self):
self.backend = OidcAdminAuthenticationBackend()
self.mock_claims = {
"sub": "00u1234567891234297",
"name": "Homer Simpson",
"lastName": "Simpson",
"firstName": "Homer",
"email": "[email protected]",
"email_verified": True,
"jobcodes": "cn=EREGS_ADMIN,ou=Groups,dc=cms,dc=hhs,dc=gov,cn=EXAMPLE_TEST,ou=Groups,dc=cms,dc=hhs,dc=gov"
}

@patch.object(OidcAdminAuthenticationBackend, 'create_user', return_value=User(email='[email protected]'))
def test_verify_claims(self, mock_create_user):
result = self.backend.verify_claims(self.mock_claims)
self.assertTrue(result)

invalid_claims = dict(self.mock_claims)
invalid_claims["email_verified"] = False
result = self.backend.verify_claims(invalid_claims)
self.assertFalse(result)

@patch.object(OidcAdminAuthenticationBackend, 'create_user')
def test_user_is_active_if_have_jobcodes(self, mock_create_user):
mock_create_user.return_value = User(email='[email protected]')
user = self.backend.create_user(self.mock_claims)
self.assertTrue(user.is_active)

def test_user_is_not_created_if_no_jobcodes(self):
self.mock_claims["jobcodes"] = ""
user = self.backend.create_user(self.mock_claims)
self.assertIsNone(user)


if __name__ == '__main':
unittest.main()
2 changes: 1 addition & 1 deletion solution/backend/resources/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def format_value(self, value):
removals = self.locations_to_strings(row["removals"])
bulk_adds = self.locations_to_strings(row["bulk_adds"])
date = parse_datetime(row["date"]).strftime("%Y-%m-%d at %I:%M %p")
output.append(f"{i+1}: On {date}, {row['user']} %s%s%s%s%s." % (
output.append(f"{i + 1}: On {date}, {row['user']} %s%s%s%s%s." % (
f"added {additions}" if additions else "",
" and " if additions and removals else "",
f"removed {removals}" if removals else "",
Expand Down