Skip to content

Commit 21c2f6a

Browse files
authored
Merge pull request #120 from docusign/feature/shared-access-example
Shared access code example
2 parents 37ce5d3 + 2d91b4c commit 21c2f6a

File tree

9 files changed

+306
-4
lines changed

9 files changed

+306
-4
lines changed

app/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
app.register_blueprint(esignature_views.eg040)
106106
app.register_blueprint(esignature_views.eg041)
107107
app.register_blueprint(esignature_views.eg042)
108+
app.register_blueprint(esignature_views.eg043)
108109

109110
if "DYNO" in os.environ: # On Heroku?
110111
import logging

app/docusign/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .ds_client import DSClient
2-
from .utils import ds_token_ok, create_api_client, authenticate, ensure_manifest, get_example_by_number, get_manifest
2+
from .utils import ds_token_ok, create_api_client, authenticate, ensure_manifest, get_example_by_number, get_manifest, \
3+
get_user_info

app/docusign/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ def wrapper(*args, **kwargs):
101101

102102
return decorator
103103

104+
def authenticate_agent(eg):
105+
def decorator(func):
106+
@wraps(func)
107+
def wrapper(*args, **kwargs):
108+
session["eg"] = url_for(eg + ".list_envelopes")
109+
110+
if ds_token_ok(minimum_buffer_min):
111+
return func(*args, **kwargs)
112+
else:
113+
return redirect(url_for("ds.ds_must_authenticate"))
114+
115+
return wrapper
116+
117+
return decorator
118+
104119
def ensure_manifest(manifest_url):
105120
def decorator(func):
106121
@wraps(func)
@@ -127,3 +142,7 @@ def is_cfr(accessToken, accountId, basePath):
127142

128143
return account_details.status21_cfr_part11
129144

145+
def get_user_info(access_token, base_path, oauth_host_name):
146+
api_client = create_api_client(base_path, access_token)
147+
api_client.set_oauth_host_name(oauth_host_name)
148+
return api_client.get_user_info(access_token)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import json
2+
3+
from docusign_esign import EnvelopesApi, UsersApi, AccountsApi, NewUsersDefinition, UserInformation, \
4+
UserAuthorizationCreateRequest, AuthorizationUser, ApiException
5+
from datetime import datetime, timedelta
6+
7+
from ...docusign import create_api_client
8+
9+
10+
class Eg043SharedAccessController:
11+
@classmethod
12+
def create_agent(cls, args):
13+
api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"])
14+
users_api = UsersApi(api_client)
15+
16+
# check if agent already exists
17+
try:
18+
users = users_api.list(args["account_id"], email=args["email"], status="Active")
19+
if int(users.result_set_size) > 0:
20+
return users.users[0]
21+
22+
except ApiException as err:
23+
error_body_json = err and hasattr(err, "body") and err.body
24+
error_body = json.loads(error_body_json)
25+
error_code = error_body and "errorCode" in error_body and error_body["errorCode"]
26+
27+
user_not_found_error_codes = ["USER_NOT_FOUND", "USER_LACKS_MEMBERSHIP"]
28+
if error_code not in user_not_found_error_codes:
29+
raise err
30+
31+
# create new agent
32+
new_users = users_api.create(args["account_id"], new_users_definition=cls.new_users_definition(args))
33+
return new_users.new_users[0]
34+
35+
@classmethod
36+
def create_authorization(cls, args):
37+
api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"])
38+
accounts_api = AccountsApi(api_client)
39+
40+
# check if authorization with manage permission already exists
41+
authorizations = accounts_api.get_agent_user_authorizations(
42+
args["account_id"],
43+
args["agent_user_id"],
44+
permissions="manage"
45+
)
46+
if int(authorizations.result_set_size) > 0:
47+
return
48+
49+
# create authorization
50+
return accounts_api.create_user_authorization(
51+
args["account_id"],
52+
args["user_id"],
53+
user_authorization_create_request=cls.user_authorization_request(args)
54+
)
55+
56+
@classmethod
57+
def new_users_definition(cls, args):
58+
agent = UserInformation(
59+
user_name=args["user_name"],
60+
email=args["email"],
61+
activation_access_code=args["activation"]
62+
)
63+
return NewUsersDefinition(new_users=[agent])
64+
65+
@classmethod
66+
def user_authorization_request(cls, args):
67+
return UserAuthorizationCreateRequest(
68+
agent_user=AuthorizationUser(
69+
account_id=args["account_id"],
70+
user_id=args["agent_user_id"]
71+
),
72+
permission="manage"
73+
)
74+
75+
@classmethod
76+
def get_envelopes(cls, args):
77+
api_client = create_api_client(base_path=args["base_path"], access_token=args["access_token"])
78+
api_client.set_default_header("X-DocuSign-Act-On-Behalf", args["user_id"])
79+
envelopes_api = EnvelopesApi(api_client)
80+
81+
from_date = (datetime.utcnow() - timedelta(days=10)).isoformat()
82+
return envelopes_api.list_status_changes(account_id=args["account_id"], from_date=from_date)

app/eSignature/views/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
from .eg040_document_visibility import eg040
4040
from .eg041_cfr_embedded_signing import eg041
4141
from .eg042_document_generation import eg042
42+
from .eg043_shared_access import eg043
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
""" Example 043: Share access to a DocuSign envelope inbox """
2+
3+
import json
4+
5+
from docusign_esign.client.api_exception import ApiException
6+
from flask import render_template, session, Blueprint, redirect, current_app, url_for, request
7+
8+
from ..examples.eg043_shared_access import Eg043SharedAccessController
9+
from ...docusign import authenticate, ensure_manifest, get_example_by_number, get_user_info
10+
from ...docusign.utils import ds_logout_internal, authenticate_agent
11+
from ...ds_config import DS_CONFIG, DS_JWT
12+
from ...error_handlers import process_error
13+
from ...consts import pattern, API_TYPE
14+
15+
example_number = 43
16+
api = API_TYPE["ESIGNATURE"]
17+
eg = f"eg0{example_number}" # reference (and url) for this example
18+
eg043 = Blueprint(eg, __name__)
19+
20+
21+
@eg043.route(f"/{eg}", methods=["POST"])
22+
@authenticate(eg=eg, api=api)
23+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
24+
def create_agent():
25+
args = {
26+
"account_id": session["ds_account_id"],
27+
"base_path": session["ds_base_path"],
28+
"access_token": session["ds_access_token"],
29+
"email": request.form.get("email"),
30+
"user_name": pattern.sub("", request.form.get("user_name")),
31+
"activation": pattern.sub("", request.form.get("activation"))
32+
}
33+
try:
34+
# 1. Create the agent user
35+
results = Eg043SharedAccessController.create_agent(args)
36+
except ApiException as err:
37+
return process_error(err)
38+
39+
session["agent_user_id"] = results.user_id
40+
41+
example = get_example_by_number(session["manifest"], example_number, api)
42+
return render_template(
43+
"example_done.html",
44+
title="Agent user created",
45+
message=example["ResultsPageText"],
46+
json=json.dumps(json.dumps(results.to_dict())),
47+
redirect_url=f"/{eg}auth"
48+
)
49+
50+
51+
@eg043.route(f"/{eg}auth", methods=["GET"])
52+
@authenticate(eg=eg, api=api)
53+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
54+
def create_authorization():
55+
user_info = get_user_info(
56+
session["ds_access_token"],
57+
session["ds_base_path"],
58+
DS_CONFIG["authorization_server"].replace("https://", "")
59+
)
60+
args = {
61+
"account_id": session["ds_account_id"],
62+
"base_path": session["ds_base_path"],
63+
"access_token": session["ds_access_token"],
64+
"user_id": user_info.sub,
65+
"agent_user_id": session["agent_user_id"]
66+
}
67+
try:
68+
# 2. Create the authorization for agent user
69+
Eg043SharedAccessController.create_authorization(args)
70+
except ApiException as err:
71+
error_body_json = err and hasattr(err, "body") and err.body
72+
error_body = json.loads(error_body_json)
73+
error_code = error_body and "errorCode" in error_body and error_body["errorCode"]
74+
if error_code == "USER_NOT_FOUND":
75+
example = get_example_by_number(session["manifest"], example_number, api)
76+
additional_page_data = next((p for p in example["AdditionalPage"] if p["Name"] == "user_not_found"),
77+
None)
78+
return render_template(
79+
"example_done.html",
80+
title="Agent user created",
81+
message=additional_page_data["ResultsPageText"],
82+
redirect_url=f"/{eg}auth"
83+
)
84+
85+
return process_error(err)
86+
87+
session["principal_user_id"] = user_info.sub
88+
89+
example = get_example_by_number(session["manifest"], example_number, api)
90+
additional_page_data = next((p for p in example["AdditionalPage"] if p["Name"] == "authenticate_as_agent"), None)
91+
return render_template(
92+
"example_done.html",
93+
title="Authenticate as the agent",
94+
message=additional_page_data["ResultsPageText"],
95+
redirect_url=f"/{eg}reauthenticate"
96+
)
97+
98+
99+
@eg043.route(f"/{eg}reauthenticate", methods=["GET"])
100+
def reauthenticate():
101+
# 3. Logout principal user and redirect to page with the list of envelopes, login as agent user
102+
ds_logout_internal()
103+
current_app.config["isLoggedIn"] = False
104+
return redirect(url_for(f"{eg}.list_envelopes"))
105+
106+
107+
@eg043.route(f"/{eg}envelopes", methods=["GET"])
108+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
109+
@authenticate_agent(eg=eg)
110+
def list_envelopes():
111+
args = {
112+
"account_id": session["ds_account_id"],
113+
"base_path": session["ds_base_path"],
114+
"access_token": session["ds_access_token"],
115+
"user_id": session["principal_user_id"]
116+
}
117+
try:
118+
# 4. Retrieve the list of envelopes
119+
results = Eg043SharedAccessController.get_envelopes(args)
120+
except ApiException as err:
121+
return process_error(err)
122+
123+
example = get_example_by_number(session["manifest"], example_number, api)
124+
125+
if int(results.result_set_size) > 0:
126+
additional_page = next((p for p in example["AdditionalPage"] if p["Name"] == "list_status_successful"), None)
127+
return render_template(
128+
"example_done.html",
129+
title="Principal's envelopes visible in the agent's Shared Access UI",
130+
message=additional_page["ResultsPageText"],
131+
json=json.dumps(json.dumps(results.to_dict()))
132+
)
133+
134+
additional_page = next((p for p in example["AdditionalPage"] if p["Name"] == "list_status_unsuccessful"), None)
135+
return render_template(
136+
"example_done.html",
137+
title="No envelopes in the principal user's account",
138+
message=additional_page["ResultsPageText"]
139+
)
140+
141+
142+
@eg043.route(f"/{eg}", methods=["GET"])
143+
@ensure_manifest(manifest_url=DS_CONFIG["example_manifest_url"])
144+
@authenticate(eg=eg, api=api)
145+
def get_view():
146+
"""responds with the form for the example"""
147+
example = get_example_by_number(session["manifest"], example_number, api)
148+
149+
return render_template(
150+
"eSignature/eg043_shared_access.html",
151+
title=example["ExampleName"],
152+
example=example,
153+
source_file="eg043_shared_access.py",
154+
source_url=DS_CONFIG["github_example_url"] + "eg043_shared_access.py",
155+
documentation=DS_CONFIG["documentation"] + eg,
156+
show_doc=DS_CONFIG["documentation"],
157+
signer_name=DS_CONFIG["signer_name"],
158+
signer_email=DS_CONFIG["signer_email"]
159+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!-- extend base layout --> {% extends "base.html" %} {% block content %}
2+
3+
{% include 'example_info.html' %}
4+
5+
{% set form_index = 0 %}
6+
{% set email_index = 0 %}
7+
{% set username_index = 1 %}
8+
{% set activation_index = 2 %}
9+
10+
<form class="eg" action="" method="post" data-busy="form">
11+
{% if 'FormName' in example['Forms'][form_index] %}
12+
<p>{{ example['Forms'][form_index]['FormName'] | safe }}</p>
13+
{% endif %}
14+
<div class="form-group">
15+
<label for="email">{{ example['Forms'][form_index]['Inputs'][email_index]['InputName'] }}</label>
16+
<input type="email" class="form-control" id="email" name="email"
17+
aria-describedby="emailHelp" placeholder="{{ example['Forms'][form_index]['Inputs'][email_index]['InputPlaceholder'] }}" required>
18+
</div>
19+
<div class="form-group">
20+
<label for="user_name">{{ example['Forms'][form_index]['Inputs'][username_index]['InputName'] }}</label>
21+
<input type="text" class="form-control" id="user_name" placeholder="{{ example['Forms'][form_index]['Inputs'][username_index]['InputPlaceholder'] }}" name="user_name"
22+
required>
23+
</div>
24+
<div class="form-group">
25+
<label for="activation">{{ example['Forms'][form_index]['Inputs'][activation_index]['InputName'] }}</label>
26+
<input type="text" class="form-control" id="activation" name="activation"
27+
placeholder="{{ example['Forms'][form_index]['Inputs'][activation_index]['InputPlaceholder'] }}" required>
28+
<small id="activationHelp" class="form-text text-muted">{{ session['manifest']['SupportingTexts']['HelpingTexts']['SaveAgentActivationCode'] | safe}}</small>
29+
</div>
30+
31+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
32+
{% include 'submit_button.html' %}
33+
</form>
34+
35+
{% endblock %}

app/templates/example_done.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ <h2>{{ title }}</h2>
1717
</ul>
1818
{% endif %}
1919

20-
<p><a href="/">Continue</a></p>
21-
20+
{% if redirect_url %}
21+
<p><a href="{{ redirect_url }}">Continue</a></p>
22+
{% else %}
23+
<p><a href="/">Continue</a></p>
24+
{% endif %}
25+
2226
{% endblock %}

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ cffi==1.15.1
44
chardet==5.1.0
55
Click
66
cryptography==39.0.0
7-
docusign-esign==3.21.0
7+
docusign-esign==3.22.0
88
docusign-rooms==1.1.0
99
docusign-monitor==1.1.0
1010
docusign-click==1.2.2

0 commit comments

Comments
 (0)