Skip to content

Commit

Permalink
#118 Pre-create Enterprise users
Browse files Browse the repository at this point in the history
  • Loading branch information
rdmccann committed Mar 28, 2023
1 parent 9495ded commit 8b0f6c0
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 39 deletions.
4 changes: 4 additions & 0 deletions AGOLAccountRequestor/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
EMAIL_BACKEND = os.environ.get('EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = os.environ.get('EMAIL_HOST', 'localhost')

PRECREATE_ENTERPRISE_USERS = os.environ.get('PRECREATE_ENTERPRISE_USERS', '').split(',') if os.environ.get('PRECREATE_ENTERPRISE_USERS') else []
# email domains which will trigger account pre creation
ENTERPRISE_USER_DOMAINS = os.environ.get('ENTERPRISE_USER_DOMAINS', [])

LOGGING = DEFAULT_LOGGING

LOGGING['handlers']['slack'] = {
Expand Down
12 changes: 10 additions & 2 deletions accounts/func.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db.models import Q
from django.utils.timezone import now
from django.conf import settings
from .models import AccountRequests, AGOL, GroupMembership, AGOLGroup, Notification
from uuid import UUID
import logging
Expand Down Expand Up @@ -83,9 +84,16 @@ def enable_account(account_request, password):
account_request.existing_account_enabled = True
account_request.save()

template = "enabled_account_email.html"
# default process is assumed to be "invite user",
template = "enabled_account_email_invited.html"

if password is not None:
# if user has epa email address and enterprise authentication is enabled, account needs to be pre-created
if str(account_request.email.split('@')[1]).lower() in settings.ENTERPRISE_USER_DOMAINS \
and settings.PRECREATE_ENTERPRISE_USERS:
template = "enabled_account_email_precreated.html"

# not an enterprise account request and password has been manually set
elif password is not None:
password_update_success = agol.update_user_account(account_request.username, {"password": password})
if password_update_success:
template = "enabled_account_email_with_password.html"
Expand Down
54 changes: 38 additions & 16 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,34 +264,52 @@ def generate_user_request_data(self, account_request, initial_password=None):
return user_request_data

def create_user_account(self, account_request, initial_password=None):
token = self.get_token()
# fail if the account already exists (don't send an invite)
if account_request.agol_id is not None:
return False

url = f'{self.portal_url}/sharing/rest/portals/self/invite/'
token = self.get_token()

user_request_data = self.generate_user_request_data(account_request, initial_password)

# goofy way of encoding data since requests library does not seem to appreciate the nested structure.
data = {
"invitationList": json.dumps({"invitations": [user_request_data]}),
"f": "json",
"token": token
}
# if user has enterprise email address and is using enterprise authentication, account needs to be pre-created
if account_request.email.split('@')[1].lower() in settings.ENTERPRISE_USER_DOMAINS \
and settings.PRECREATE_ENTERPRISE_USERS:

if initial_password is None:
template = get_template('invitation_email_body.html')
data["message"] = template.render({
"account_request": account_request,
"PORTAL": self
url = f'{self.portal_url}/portaladmin/security/users/createUser'
user_request_data.update({
"provider": "enterprise",
"userLicenseTypeId": "creatorUT",
"f": "json",
"token": token
})
data = user_request_data

else: # Invite user
url = f'{self.portal_url}/sharing/rest/portals/self/invite/'

# goofy way of encoding data since requests library does not seem to appreciate the nested structure.
data = {
"invitationList": json.dumps({"invitations": [user_request_data]}),
"f": "json",
"token": token
}

if initial_password is None:
template = get_template('invitation_email_body.html')
data["message"] = template.render({
"account_request": account_request,
"PORTAL": self
})

# pre-encode to ensure nested data isn't lost
data = urlencode(data)
response = requests.post(url, data=data, headers={'Content-type': 'application/x-www-form-urlencoded'})

response_json = response.json()

if 'success' in response_json and response_json['success'] \
and account_request.username not in response_json['notInvited']:
if 'success' in response_json and response_json['success'] or response_json['status'] == 'success':
# and account_request.username not in response_json['notInvited']:
user_url = f'{self.portal_url}/sharing/rest/community/users/{account_request.username}'
user_response = requests.get(user_url, params={'token': token, 'f': 'json'})
user_response_json = user_response.json()
Expand All @@ -301,7 +319,11 @@ def create_user_account(self, account_request, initial_password=None):
account_request.agol_id = user_response_json['id']
account_request.save(update_fields=['agol_id'])

return True
return True
else:
# something else went wrong TODO log the error
print(response_json)
return False

def check_username(self, username):
token = self.get_token()
Expand Down
7 changes: 7 additions & 0 deletions accounts/templates/enabled_account_email_precreated.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Your EPA {{ PORTAL }} account, {{ username }}, has been enabled as part of the {{ response }} response / project.
<br/><br/>
Please click here to access <a href="{{ portal_url }}/home/signin.html?useLandingPage=true">EPA {{ PORTAL }}</a>.
<br/><br/>
You may need to use Login.gov credentials if you are accessing this outside of the EPA intranet/VDI environment.

{% include "signature_line.html" %}
7 changes: 5 additions & 2 deletions accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@


def format_username(data):
username_extension = 'EPAEXT' if '@epa.gov' not in data['email'] else 'EPA'
username = f'{data["last_name"].capitalize()}.{data["first_name"].capitalize()}_{username_extension}'
if data['email'].split('@')[1].lower() in settings.ENTERPRISE_USER_DOMAINS:
username = data['email']
else:
username_extension = 'EPAEXT' if '@epa.gov' not in data['email'] else 'EPA'
username = f'{data["last_name"].capitalize()}.{data["first_name"].capitalize()}_{username_extension}'
return username.replace(' ', '')


Expand Down
53 changes: 34 additions & 19 deletions ui/src/app/approval-list/approval-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Accounts {
styleUrls: ['./approval-list.component.css']
})
export class ApprovalListComponent implements OnInit {
userConfig: UserConfig;
accounts: BaseService;
displayedColumns = ['selected', 'username', 'first_name', 'last_name', 'email', 'organization', 'groups', 'response',
'sponsor', 'reason', 'approved', 'created', 'delete'];
Expand All @@ -59,8 +60,10 @@ export class ApprovalListComponent implements OnInit {
searchInput = new FormControl(null);

constructor(public http: HttpClient, loadingService: LoadingService,
public dialog: MatDialog = null, public loginService: LoginService = null,
private toastr: ToastrService) {
public dialog: MatDialog = null,
public loginService: LoginService = null,
public UserConfigService: UserConfigService,
public matSnackBar: MatSnackBar) {
this.accounts = new BaseService('v1/account/approvals', http, loadingService);
}

Expand All @@ -71,6 +74,8 @@ export class ApprovalListComponent implements OnInit {
// set accounts list record properties
// this.setAccountsListProps();

this.UserConfigService.config.subscribe(c => this.userConfig = c);

this.accounts.filter = {created: false};
this.accounts.dataChange.pipe(
tap(response => this.setAccountsListProps(response))
Expand Down Expand Up @@ -255,28 +260,38 @@ export class ApprovalListComponent implements OnInit {
}

openApproveOptions() {
this.dialog.open(ChooseCreationMethodComponent).afterClosed().pipe(
filter(x => x),
switchMap(choice => {
if (choice === 'password') {
return this.openSetPasswordDialog();
} else if (choice === 'invitation') {
return this.confirmSendInvitation().pipe(map(x => {
return {confirmed: x, password: null};
}));
}
return of({confirmed: false});
}),
filter(x => x.confirmed),
tap(r => this.createAccounts(r.password)),
).subscribe();
if(this.userConfig.portal.toLowerCase() == 'geoplatform') {
this.dialog.open(ChooseCreationMethodComponent).afterClosed().pipe(
filter(x => x),
switchMap(choice => {
if (choice === 'password') {
return this.openSetPasswordDialog();
} else if (choice === 'invitation') {
return this.confirmSendNotification().pipe(map(x => {
return {confirmed: x, password: null};
}));
}
return of({confirmed: false});
}),
filter(x => x.confirmed),
tap(r => this.createAccounts(r.password))
).subscribe();
} else if(this.userConfig.portal.toLowerCase() == 'geosecure'){
this.confirmSendNotification().pipe(
map(x => {
return {confirmed: x, password: null};
}),
tap(r => this.createAccounts(null))
).subscribe();
}
}

confirmSendInvitation(): Observable<boolean> {
confirmSendNotification(): Observable<boolean> {
return this.dialog.open(GenericConfirmDialogComponent, {
width: '400px',
data: {
message: 'This will send auto-generated emails to the approved accounts. Each user must click the link in their email to finish setting up their account. ' +
// message: message
message: 'This will send auto-generated emails to the approved accounts. Additional instructions will be included in the email for the end user. ' +
'Please confirm you want to send emails now.'
}
}).afterClosed();
Expand Down

0 comments on commit 8b0f6c0

Please sign in to comment.