Simply and improve consistency

This commit is contained in:
Mathieu Leplatre 2017-11-23 16:32:53 +01:00
Родитель 2cd6ba2119
Коммит 88373e250f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 767B105F81A15CDD
10 изменённых файлов: 168 добавлений и 196 удалений

Просмотреть файл

@ -5,7 +5,8 @@ Doorman
*Doorman* is an **authorization micro-service**.
[API Documentation](https://mozilla.github.io/doorman/)
- [API Documentation](https://mozilla.github.io/doorman/)
- [Examples](examples/)
[![Build Status](https://travis-ci.org/mozilla/doorman.svg?branch=master)](https://travis-ci.org/mozilla/doorman)
[![Coverage Status](https://coveralls.io/repos/github/mozilla/doorman/badge.svg?branch=master)](https://coveralls.io/github/mozilla/doorman?branch=master)

Просмотреть файл

@ -1,5 +1,4 @@
# Doorman Integration Examples
Here are some examples of how to integrate Doorman with your service.
- [Python / Flask](python/): A Web UI interacts with Auth0 and a Flask API
- [Python / Flask example](python/)

Просмотреть файл

@ -8,6 +8,4 @@ name = "pypi"
[packages]
flask = "*"
"python-dotenv" = "*"
"python-jose" = "*"
"flask-cors" = "*"

51
examples/python/Pipfile.lock сгенерированный
Просмотреть файл

@ -1,19 +1,19 @@
{
"_meta": {
"hash": {
"sha256": "8945c85e8436bb5f825358403b459dd640c494b819b6f09a474653091c02f14e"
"sha256": "f7aa6b6f4306185cc521b1df2b2db72cf6053464e44784087db3fa20208ca4f2"
},
"host-environment-markers": {
"implementation_name": "cpython",
"implementation_version": "3.5.3",
"implementation_version": "3.6.3",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "4.10.0-33-generic",
"platform_release": "4.13.0-16-generic",
"platform_system": "Linux",
"platform_version": "#37-Ubuntu SMP Fri Aug 11 10:55:28 UTC 2017",
"python_full_version": "3.5.3",
"python_version": "3.5",
"platform_version": "#19-Ubuntu SMP Wed Oct 11 18:35:14 UTC 2017",
"python_full_version": "3.6.3",
"python_version": "3.6",
"sys_platform": "linux"
},
"pipfile-spec": 6,
@ -34,13 +34,6 @@
],
"version": "==6.7"
},
"ecdsa": {
"hashes": [
"sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c",
"sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa"
],
"version": "==0.13"
},
"flask": {
"hashes": [
"sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
@ -56,12 +49,6 @@
],
"version": "==3.0.3"
},
"future": {
"hashes": [
"sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
],
"version": "==0.16.0"
},
"itsdangerous": {
"hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
@ -70,10 +57,10 @@
},
"jinja2": {
"hashes": [
"sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054",
"sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff"
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
],
"version": "==2.9.6"
"version": "==2.10"
},
"markupsafe": {
"hashes": [
@ -81,26 +68,6 @@
],
"version": "==1.0"
},
"pycrypto": {
"hashes": [
"sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"
],
"version": "==2.6.1"
},
"python-dotenv": {
"hashes": [
"sha256:dc7940052cfe170e881aea40feb4ea7776e6a97170ed038fd2ee7e26e47585f2",
"sha256:45e927c34204c90f5faa35ea8709b894f6b1a7712d77eb50940601068040993b"
],
"version": "==0.7.1"
},
"python-jose": {
"hashes": [
"sha256:fed56224664af0ebc3947853f1bed23b5609f90c7b02e3dce5ef5757d0301664",
"sha256:18e19f200f37a8ee6247921572807cc63ee034abdbf6854df1ae7c1f505cabcc"
],
"version": "==1.4.0"
},
"six": {
"hashes": [
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",

Просмотреть файл

@ -1,28 +1,38 @@
# Doorman, Python / Flask example with a Web UI
# Doorman + Python API + Web UI
## How to run doorman?
A Web UI interacts with Auth0 and a Flask API:
* Some views are protected by a Python decorator
* The update view is protected by imperative code, where authors can only update their own records
## Run locally
Run those three services in separate terminals:
### Doorman
make serve -e POLICIES=examples/python/policies.yaml
### Flask API
## How to run the server
We use [Pipenv](https://docs.pipenv.org) to ease packages installation.
cd examples/python/
pipenv install
export DOORMAN_SERVER=http://localhost:8080
export API_AUDIENCE="SLocf7Sa1ibd5GNJMMqO539g7cKvWBOI"
pipenv run python server.py
## How to run the web UI
### Web UI
Because of Auth0 configuration, we must access the Web UI on http://iam.local:3000/
- Add this line to your `/etc/hosts`:
127.0.0.1 iam.local
- Serve the UI static files:
cd examples/python/ui/
python3 -m http.server 3000
- Update your `/etc/hosts` so that you can resolve `iam.local`:
127.0.0.1 iam.local
- Access http://iam.local:3000/
- Click **Login**

Просмотреть файл

@ -2,20 +2,15 @@ import json
import urllib
def json_dumps_ignore_none(d):
return json.dumps({k: v for k, v in d.items() if v is not None})
# Format error response and append status code.
class AuthZError(Exception):
def __init__(self, error, status_code):
self.error = error
self.status_code = status_code
def allowed(server, audience, *,
def allowed(doorman, service, *,
resource=None, action=None, jwt=None, principals=None, context=None):
doorman_url = server + "/allowed"
doorman_url = doorman + "/allowed"
payload = {
"resource": resource,
"action": action,
@ -25,7 +20,7 @@ def allowed(server, audience, *,
body = json_dumps_ignore_none(payload)
headers = {
"Authorization": jwt or '',
"Origin": audience,
"Origin": service,
}
r = urllib.request.Request(doorman_url, data=body.encode("utf-8"), headers=headers)
try:
@ -39,3 +34,7 @@ def allowed(server, audience, *,
raise AuthZError(response_body, 403)
return response_body
def json_dumps_ignore_none(d):
return json.dumps({k: v for k, v in d.items() if v is not None})

Просмотреть файл

@ -1,7 +1,7 @@
service: SLocf7Sa1ibd5GNJMMqO539g7cKvWBOI
jwtIssuer: https://auth.mozilla.auth0.com/
policies:
- id: "1"
- id: "hello"
description: Allow everyone access hello
principals:
- <.*>
@ -10,7 +10,7 @@ policies:
resources:
- hello
effect: allow
- id: "2"
- id: "record-everyone"
description: Allow everyone to list, read and create records
principals:
- <.*>
@ -21,7 +21,7 @@ policies:
resources:
- record
effect: allow
- id: "3"
- id: "record-authors"
description: Allow authors to update their own record
principals:
- <.*>

Просмотреть файл

@ -13,14 +13,14 @@ import werkzeug
import doorman
DOORMAN_SERVER = os.getenv("DOORMAN_SERVER", "http://localhost:8080")
API_AUDIENCE = os.getenv("API_AUDIENCE")
SERVICE = os.getenv("SERVICE", "SLocf7Sa1ibd5GNJMMqO539g7cKvWBOI")
HERE = os.path.abspath(os.path.dirname(__file__))
RECORDS_PATH = os.getenv("RECORDS_PATH", os.path.join(HERE, "records"))
app = Flask(__name__)
allowed = functools.partial(doorman.allowed, DOORMAN_SERVER, API_AUDIENCE)
allowed = functools.partial(doorman.allowed, DOORMAN_SERVER, SERVICE)
@app.errorhandler(doorman.AuthZError)
@ -35,8 +35,8 @@ def authorized(**allowed_kw):
@functools.wraps(f)
def wrapper(*args, **kwargs):
jwt = request.headers.get("Authorization", None)
payload = allowed(jwt=jwt, **allowed_kw)
_app_ctx_stack.top.current_user = payload
authz = allowed(jwt=jwt, **allowed_kw)
_app_ctx_stack.top.authz = authz
return f(*args, **kwargs)
return wrapper
return wrapped
@ -49,31 +49,28 @@ def authorized(**allowed_kw):
def hello():
"""A valid access token is required to access this route
"""
top = _app_ctx_stack.top
return jsonify(top.current_user)
authz = _app_ctx_stack.top.authz
return jsonify(authz)
@app.route("/records")
@cross_origin(headers=["Content-Type", "Authorization"])
@cross_origin(headers=["Access-Control-Allow-Origin", "*"])
@authorized(resource="record", action="list")
def records():
jwt = request.headers.get("Authorization", None)
# Check if allowed to list.
authz = allowed(resource="record", action="list", jwt=jwt)
authz = _app_ctx_stack.top.authz
email_principal = authz["principals"][1]
records = Records.list(email_principal)
records = Records.list(author=email_principal)
return jsonify(records)
@app.route("/records/<record_id>", methods=('GET', 'PUT'))
@app.route("/records/<name>", methods=('GET', 'PUT'))
@cross_origin(headers=["Content-Type", "Authorization"])
@cross_origin(headers=["Access-Control-Allow-Origin", "*"])
def record(record_id):
def record(name):
jwt = request.headers.get("Authorization", None)
record, author = Records.read(record_id)
record, author = Records.read(name)
if request.method == "GET":
action = "read"
@ -89,9 +86,9 @@ def record(record_id):
# Save content on PUT
if request.method == "PUT":
record = request.get_json()
body = request.data.decode("utf-8")
email_principal = authz["principals"][1]
Records.save(record_id, record, email_principal)
record = Records.save(name, body, email_principal)
return jsonify(record)
@ -116,13 +113,14 @@ class Records:
@staticmethod
def save(name, body, author):
path = os.path.join(RECORDS_PATH, "{}.json".format(os.path.basename(name)))
body = {'body': body, 'author': author}
with open(path, 'w') as f:
body = {'body': body, 'author': author}
json.dump(body, f)
return body
if __name__ == "__main__":
print("RECORDS_PATH", RECORDS_PATH)
print("DOORMAN_SERVER", DOORMAN_SERVER)
print("API_AUDIENCE", API_AUDIENCE)
print("SERVICE", SERVICE)
app.run(host="0.0.0.0", port=os.getenv("PORT", 8000))

Просмотреть файл

@ -7,23 +7,37 @@
<script type="text/javascript" src="main.js"></script>
</head>
<body>
<h1>Doorman + Python API + Frontend</h1>
<h1>Doorman + Python API + Web UI</h1>
<button id="login">Login</button>
<button id="logout">Logout</button>
<div id="view" class="tabs">
<div class="tab">
<input type="radio" id="tab-1" name="tab-group-1" checked>
<label for="tab-1">Token payload</label>
<input type="radio" id="tab-1" name="tab-group-1">
<label for="tab-1">API Hello</label>
<div class="content">
<div id="token-payload" class="pre"></div>
<div id="api-hello" class="pre"></div>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-2" name="tab-group-1">
<label for="tab-2">User Info</label>
<label for="tab-2">API Records</label>
<div class="content">
<form id="api-record-form">
<input type="text" name="name" placeholder="Name..." />
<input type="text" name="data" placeholder="Some data..."></textarea>
<input type="submit" value="Save"/>
</form>
<div id="api-records"></div>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-3" name="tab-group-1">
<label for="tab-3">User Info</label>
<div class="content">
<div id="profile-nickname"></div>
@ -33,25 +47,11 @@
</div>
<div class="tab">
<input type="radio" id="tab-3" name="tab-group-1">
<label for="tab-3">API Hello</label>
<input type="radio" id="tab-4" name="tab-group-1" checked>
<label for="tab-4">Token payload</label>
<div class="content">
<div id="api-hello" class="pre"></div>
</div>
</div>
<div class="tab">
<input type="radio" id="tab-4" name="tab-group-1">
<label for="tab-4">API Records</label>
<div class="content">
<form id="api-record-form">
<input type="text" name="name" placeholder="Name..." />
<textarea name="body" placeholder="Some JSON..."></textarea>
<input type="submit" value="Save"/>
</form>
<div id="api-records"></div>
<div id="token-payload" class="pre"></div>
</div>
</div>
</div>

Просмотреть файл

@ -9,10 +9,7 @@ const SCOPES = 'openid profile';
document.addEventListener('DOMContentLoaded', main);
function main() {
const logoutBtn = document.getElementById('logout');
logoutBtn.addEventListener('click', logout);
const webAuth = new auth0.WebAuth({
const webAuth0 = new auth0.WebAuth({
domain: AUTH0_DOMAIN,
clientID: AUTH0_CLIENT_ID,
redirectUri: AUTH0_CALLBACK_URL,
@ -20,57 +17,89 @@ function main() {
scope: SCOPES
});
// Authentication on Login button
// Start authentication process on Login button
const loginBtn = document.getElementById('login');
loginBtn.addEventListener('click', () => {
webAuth.authorize();
webAuth0.authorize();
});
// Logout button.
const logoutBtn = document.getElementById('logout');
logoutBtn.addEventListener('click', logout);
// New record form.
const newRecordForm = document.getElementById('api-record-form');
newRecordForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(newRecordForm);
await postNewRecords(formData.get('name'), formData.get('body'))
// Empty form once submitted.
newRecordForm.reset()
});
handleAuthentication(webAuth)
handleAuthentication(webAuth0)
}
function handleAuthentication(webAuth) {
webAuth.parseHash((err, authResult) => {
class APIClient {
constructor(auth) {
const headers = {
'Authorization': `${auth.tokenType} ${auth.idToken}`,
};
this.options = {headers};
}
async hello() {
const resp = await fetch(`${SERVICE_URL}/`, this.options);
return await resp.json();
}
async list() {
const resp = await fetch(`${SERVICE_URL}/records`, this.options);
return await resp.json();
}
async save(name, body) {
const resp = await fetch(`${SERVICE_URL}/records/${name}`,
{method: 'PUT', body, ...this.options});
return await resp.json();
}
}
function handleAuthentication(webAuth0) {
let authenticated = false;
webAuth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
// Token was passed in location hash by authentication portal.
authenticated = true;
window.location.hash = '';
setSession(authResult);
} else if (err) {
// Server returned an error.
console.error(err);
alert(
'Error: ' + err.error + '. Check the console for further details.'
);
alert(`Error: ${err.error}. Check the console for further details.`);
} else {
authResult = JSON.parse(sessionStorage.getItem('session'));
// Look into session storage for session.
const expiresAt = JSON.parse(sessionStorage.getItem('expires_at'));
// Check whether the current time is past the access token's expiry time
if (new Date().getTime() < expiresAt) {
authenticated = true;
authResult = JSON.parse(sessionStorage.getItem('session'));
}
}
displayButtons()
// Show/hide menus.
displayButtons(authenticated)
if (isAuthenticated()) {
// Interact with API if authenticated.
if (authenticated) {
console.log('AuthResult', authResult);
const tokenPayloadDiv = document.getElementById('token-payload');
tokenPayloadDiv.innerText = JSON.stringify(authResult.idTokenPayload, null, 2);
showTokenPayload(authResult)
const apiClient = new APIClient(authResult);
initRecordForm(apiClient)
Promise.all([
fetchUserInfo(webAuth),
showAPIHello(),
showAPIRecords(),
fetchUserInfo(webAuth0, authResult),
showAPIHello(apiClient),
showAPIRecords(apiClient),
]);
}
});
}
function displayButtons() {
if (isAuthenticated()) {
function displayButtons(authenticated) {
if (authenticated) {
document.getElementById('login').setAttribute('disabled', 'disabled');
document.getElementById('logout').removeAttribute('disabled');
document.getElementById('view').style.display = 'block';
@ -90,28 +119,18 @@ function setSession(authResult) {
sessionStorage.setItem('expires_at', expiresAt);
}
function isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
const expiresAt = JSON.parse(sessionStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
function logout() {
// Remove tokens and expiry time from sessionStorage
sessionStorage.removeItem('session');
sessionStorage.removeItem('expires_at');
displayButtons();
displayButtons(false);
}
async function fetchUserInfo(webAuth) {
const auth = JSON.parse(sessionStorage.getItem('session'));
webAuth.client.userInfo(auth.accessToken, (err, profile) => {
async function fetchUserInfo(webAuth0, auth) {
webAuth0.client.userInfo(auth.accessToken, (err, profile) => {
if (err) {
console.error(err);
alert(
'Error: ' + err.error + '. Check the console for further details.'
);
alert(`Error: ${err.error}. Check the console for further details.`);
}
document.getElementById('profile-nickname').innerText = profile.nickname;
document.getElementById('profile-picture').setAttribute('src', profile.picture);
@ -119,67 +138,48 @@ async function fetchUserInfo(webAuth) {
});
}
class APIClient {
constructor() {
const auth = JSON.parse(sessionStorage.getItem('session'));
this.headers = {
'Authorization': `${auth.tokenType} ${auth.idToken}`,
'Content-Type': 'application/json'
};
}
async hello() {
const resp = await fetch(`${SERVICE_URL}/`, {headers: this.headers});
return await resp.json();
}
async list() {
const resp = await fetch(`${SERVICE_URL}/records`, {headers: this.headers});
return await resp.json();
}
async save(name, body) {
const resp = await fetch(`${SERVICE_URL}/records/${name}`,
{method: 'PUT', body, headers: this.headers});
return await resp.json();
}
function showTokenPayload(auth) {
const tokenPayloadDiv = document.getElementById('token-payload');
tokenPayloadDiv.innerText = JSON.stringify(auth.idTokenPayload, null, 2);
}
async function showAPIHello() {
const c = new APIClient();
const data = await c.hello();
async function showAPIHello(apiClient) {
const data = await apiClient.hello();
const apiHelloDiv = document.getElementById('api-hello');
apiHelloDiv.innerText = JSON.stringify(data, null, 2);
}
async function showAPIRecords() {
const c = new APIClient();
const data = await c.list();
function initRecordForm(apiClient) {
const newRecordForm = document.getElementById('api-record-form');
// Submit data.
newRecordForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(newRecordForm);
await apiClient.save(formData.get('name'), formData.get('data'));
// Empty form once submitted.
newRecordForm.reset()
// Refresh list.
await showAPIRecords(apiClient);
});
}
async function showAPIRecords(apiClient) {
const apiRecordsDiv = document.getElementById('api-records');
apiRecordsDiv.innerHTML = '';
const data = await apiClient.list();
if (data.length == 0) {
apiRecordsDiv.innerText = 'No records';
return
}
apiRecordsDiv.innerHTML = '';
for (const {name, body} of data) {
const _name = document.createElement('h2');
_name.innerText = name;
const _body = document.createElement('p');
_body.className = 'pre';
_body.innerText = JSON.stringify(body, null, 2);
_body.innerText = body;
apiRecordsDiv.appendChild(_name);
apiRecordsDiv.appendChild(_body);
}
}
async function postNewRecords(name, body) {
const c = new APIClient();
await c.save(name, body);
// Refresh list.
await showAPIRecords();
}