-
Notifications
You must be signed in to change notification settings - Fork 124
/
flask_app_with_oauth.py
executable file
·162 lines (129 loc) · 6.55 KB
/
flask_app_with_oauth.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!env python3
""" Flask with OAuth:
This application provides end-to-end demonstration of Databricks SDK for Python
capabilities of OAuth Authorization Code flow with PKCE security enabled. This
can help you build a hosted app with every user using their own identity to
access Databricks resources.
If you have already Custom App:
./flask_app_with_oauth.py --host <databricks workspace url> \
--client_id <app-client-id> \
--client_secret <app-secret> \
--port 5001
If you want this script to register Custom App and redirect URL for you:
./flask_app_with_oauth.py --port 5001 --profile <databricks account profile>
You'll get prompted for Databricks Account username and password for
script to enroll your account into OAuth and create a custom app with
http://localhost:5001/callback as the redirect callback. Client and
secret credentials for this OAuth app will be printed to the console,
so that you could resume testing this app at a later stage.
Once started, please open http://localhost:5001 in your browser and
go through SSO flow to get a list of clusters on <databricks workspace url>.
"""
import argparse
import logging
import sys
from databricks.sdk.oauth import OAuthClient, get_workspace_endpoints
from databricks.sdk.service.compute import ListClustersFilterBy, State
APP_NAME = "flask-demo"
all_clusters_template = """<ul>
{% for cluster in clusters -%}
<li><a
target="_blank"
href="{{ workspace_host }}/#setting/clusters/{{ cluster.cluster_id }}/configuration">
{{ cluster.cluster_name }}</a> is {{ cluster.state }}</li>
{% endfor %}
</ul>"""
def create_flask_app(workspace_host: str, client_id: str, client_secret: str):
"""The create_flask_app function creates a Flask app that is enabled with OAuth.
It initializes the app and web session secret keys with a randomly generated token. It defines two routes for
handling the callback and index pages.
"""
import secrets
from flask import (Flask, redirect, render_template_string, request,
session, url_for)
app = Flask(APP_NAME)
app.secret_key = secrets.token_urlsafe(32)
@app.route("/callback")
def callback():
"""The callback route initiates consent using the OAuth client, exchanges
the callback parameters, and redirects the user to the index page."""
from databricks.sdk.oauth import Consent
consent = Consent.from_dict(session["consent"], client_secret=client_secret)
session["creds"] = consent.exchange_callback_parameters(request.args).as_dict()
return redirect(url_for("index"))
@app.route("/")
def index():
"""The index page checks if the user has already authenticated and retrieves the user's credentials using
the Databricks SDK WorkspaceClient. It then renders the template with the clusters' list."""
oidc_endpoints = get_workspace_endpoints(workspace_host)
port = request.environ.get("SERVER_PORT")
redirect_url=f"http://localhost:{port}/callback"
if "creds" not in session:
oauth_client = OAuthClient(oidc_endpoints=oidc_endpoints,
client_id=client_id,
client_secret=client_secret,
redirect_url=redirect_url)
consent = oauth_client.initiate_consent()
session["consent"] = consent.as_dict()
return redirect(consent.authorization_url)
from databricks.sdk import WorkspaceClient
from databricks.sdk.oauth import SessionCredentials
credentials_strategy = SessionCredentials.from_dict(session["creds"],
token_endpoint=oidc_endpoints.token_endpoint,
client_id=client_id,
client_secret=client_secret,
redirect_url=redirect_url)
workspace_client = WorkspaceClient(host=workspace_host,
product=APP_NAME,
credentials_strategy=credentials_strategy,
)
clusters = workspace_client.clusters.list(
filter_by=ListClustersFilterBy(cluster_states=[State.RUNNING, State.PENDING])
)
return render_template_string(all_clusters_template, workspace_host=workspace_host, clusters=clusters)
return app
def register_custom_app(args: argparse.Namespace) -> tuple[str, str]:
"""Creates new Custom OAuth App in Databricks Account"""
logging.info("No OAuth custom app client/secret provided, creating new app")
from databricks.sdk import AccountClient
account_client = AccountClient(profile=args.profile)
custom_app = account_client.custom_app_integration.create(
name=APP_NAME,
redirect_urls=[
f"http://localhost:{args.port}/callback",
],
confidential=True,
scopes=["all-apis"],
)
logging.info(f"Created new custom app: "
f"--client_id {custom_app.client_id} "
f"--client_secret {custom_app.client_secret}")
return custom_app.client_id, custom_app.client_secret
def parse_arguments() -> argparse.Namespace:
"""Parses arguments for this demo"""
parser = argparse.ArgumentParser(prog=APP_NAME, description=__doc__.strip())
parser.add_argument("--host")
for flag in ["client_id", "client_secret"]:
parser.add_argument(f"--{flag}")
parser.add_argument("--port", default=5001, type=int)
parser.add_argument("--profile", default="DEFAULT", help="Databricks account profile to use for authentication.")
return parser.parse_args()
if __name__ == "__main__":
logging.basicConfig(stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s [%(name)s][%(levelname)s] %(message)s",
)
logging.getLogger("databricks.sdk").setLevel(logging.DEBUG)
args = parse_arguments()
client_id, client_secret = args.client_id, args.client_secret
if not client_id:
client_id, client_secret = register_custom_app(args)
app = create_flask_app(args.host, client_id, client_secret)
app.run(
host="localhost",
port=args.port,
debug=True,
# to simplify this demo experience, we create OAuth Custom App for you,
# but it intervenes with the werkzeug reloader. So we disable it
use_reloader=args.client_id is not None,
)