Skip to content

Commit bcac9b6

Browse files
authored
Merge pull request #20 from CSCfi/feature/authentication
Merge authentication support
2 parents f89c1d4 + 3daee70 commit bcac9b6

File tree

6 files changed

+223
-9
lines changed

6 files changed

+223
-9
lines changed

Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ COPY ./deploy/app.sh /app/app.sh
3535

3636
RUN chmod +x /app/app.sh
3737

38+
RUN adduser --disabled-password --no-create-home swiftsharing
39+
USER swiftsharing
40+
3841
ENTRYPOINT ["/bin/sh", "-c", "/app/app.sh"]

bindings/js/swift_x_account_sharing_bind.js

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ class SwiftXAccountSharing {
44
// Swift cross account sharing backend client.
55

66
constructor (
7-
address
7+
address,
8+
signatureAddress = "",
89
) {
910
this.address = address;
11+
this.signatureAddress = signatureAddress;
1012
}
1113

1214
_parseListString (
@@ -20,11 +22,37 @@ class SwiftXAccountSharing {
2022
return ret.slice(0, ret.length - 1);
2123
}
2224

25+
async _getSignature(
26+
validFor,
27+
toSign
28+
) {
29+
// Get a signature for an API call.
30+
if (this.signatureAddress != "") {
31+
let signatureUrl = new URL("/sign/".concat(validFor), this.signatureAddress);
32+
signatureUrl.searchParams.append("path", toSign);
33+
let signed = await fetch(
34+
signatureUrl, {method: "GET", credentials: "same-origin"}
35+
);
36+
return signed.json();
37+
}
38+
else {
39+
return undefined;
40+
}
41+
}
42+
2343
async getAccess (
2444
username
2545
) {
2646
// List the containers the user has been given access to.
2747
let url = new URL("/access/".concat(username), this.address);
48+
49+
let signed = await this._getSignature(
50+
60,
51+
"/access/".concat(username)
52+
);
53+
url.searchParams.append("valid", signed.valid_until);
54+
url.searchParams.append("signature", signed.signature);
55+
2856
let containers = fetch(
2957
url, {method: "GET"}
3058
).then(
@@ -42,6 +70,14 @@ class SwiftXAccountSharing {
4270
let url = new URL(
4371
"/access/".concat(username, "/", container), this.address
4472
);
73+
74+
let signed = await this._getSignature(
75+
60,
76+
"/access/".concat(username, "/", container)
77+
);
78+
url.searchParams.append("valid", signed.valid_until);
79+
url.searchParams.append("signature", signed.signature);
80+
4581
url.searchParams.append("owner", owner);
4682
let details = fetch(
4783
url, {method: "GET"}
@@ -56,6 +92,14 @@ class SwiftXAccountSharing {
5692
) {
5793
// List the containers the user has shared to another user / users.
5894
let url = new URL("/share/".concat(username), this.address);
95+
96+
let signed = await this._getSignature(
97+
60,
98+
"/share/".concat(username)
99+
);
100+
url.searchParams.append("valid", signed.valid_until);
101+
url.searchParams.append("signature", signed.signature);
102+
59103
let shared = fetch(
60104
url, {method: "GET"}
61105
).then(
@@ -72,6 +116,14 @@ class SwiftXAccountSharing {
72116
let url = new URL(
73117
"/share/".concat(username, "/", container), this.address
74118
);
119+
120+
let signed = await this._getSignature(
121+
60,
122+
"/share/".concat(username, "/", container)
123+
);
124+
url.searchParams.append("valid", signed.valid_until);
125+
url.searchParams.append("signature", signed.signature);
126+
75127
let details = fetch(
76128
url, {method: "GET"}
77129
).then(
@@ -94,6 +146,14 @@ class SwiftXAccountSharing {
94146
url.searchParams.append("user", this._parseListString(userlist));
95147
url.searchParams.append("access", this._parseListString(accesslist));
96148
url.searchParams.append("address", address);
149+
150+
let signed = await this._getSignature(
151+
60,
152+
"/share/".concat(username, "/", container)
153+
);
154+
url.searchParams.append("valid", signed.valid_until);
155+
url.searchParams.append("signature", signed.signature);
156+
97157
let shared = fetch(
98158
url, {method: "POST"}
99159
).then(
@@ -114,6 +174,14 @@ class SwiftXAccountSharing {
114174
);
115175
url.searchParams.append("user", this._parseListString(userlist));
116176
url.searchParams.append("access", this._parseListString(accesslist));
177+
178+
let signed = await this._getSignature(
179+
60,
180+
"/share/".concat(username, "/", container)
181+
);
182+
url.searchParams.append("valid", signed.valid_until);
183+
url.searchParams.append("signature", signed.signature);
184+
117185
let shared = fetch(
118186
url, {method: "PATCH"}
119187
).then(
@@ -132,6 +200,14 @@ class SwiftXAccountSharing {
132200
"/share/".concat(username, "/", container), this.address
133201
);
134202
url.searchParams.append("user", this._parseListString(userlist));
203+
url.searchParams.append("valid", signed.valid_until);
204+
url.searchParams.append("signature", signed.signature);
205+
206+
let signed = await this._getSignature(
207+
60,
208+
"/share/".concat(username, "/", container)
209+
);
210+
135211
let deleted = fetch(
136212
url, {method: "DELETE"}
137213
).then(

swift_x_account_sharing/auth.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Authentication module for the swift-sharing-request."""
2+
3+
# The authentication is done via requests signed with a pre-whitelisted token.
4+
# This is done to prevent invalid usage. New tokens can be created either
5+
# manually on the platform that's running the software, or by
6+
# pre-authenticating via Openstack Keystone.
7+
8+
# Authentication engine is written as an aiohttp middleware function.
9+
10+
11+
import os
12+
import typing
13+
import hmac
14+
15+
import aiohttp.web
16+
17+
18+
AiohttpHandler = typing.Callable[[aiohttp.web.Request], aiohttp.web.Response]
19+
20+
21+
async def read_in_keys(
22+
app: aiohttp.web.Application
23+
):
24+
"""Read in keys to the application."""
25+
keys = os.environ.get("SWIFT_UI_API_AUTH_TOKENS", None)
26+
app["tokens"] = keys.split(",") if keys is not None else []
27+
if app["tokens"]:
28+
app["tokens"] = [
29+
token.encode("utf-8") for token in app["tokens"]
30+
]
31+
32+
33+
async def test_signature(
34+
tokens: typing.List[bytes],
35+
signature: str,
36+
message: str,
37+
) -> bool:
38+
"""Validate signature against the given tokens."""
39+
byte_message = message.encode("utf-8")
40+
for token in tokens:
41+
digest = hmac.new(
42+
token,
43+
byte_message,
44+
digestmod="sha256"
45+
).hexdigest()
46+
if digest == signature:
47+
return
48+
raise aiohttp.web.HTTPUnauthorized(
49+
reason="Missing valid query signature"
50+
)
51+
52+
53+
@aiohttp.web.middleware
54+
async def handle_validate_authentication(
55+
request: aiohttp.web.Request,
56+
handler: AiohttpHandler,
57+
) -> aiohttp.web.Response:
58+
"""Handle the authentication of a response as a middleware function."""
59+
try:
60+
signature = request.query["signature"]
61+
validity = request.query["valid"]
62+
path = request.url.path
63+
except KeyError:
64+
raise aiohttp.web.HTTPClientError(
65+
reason="Query string missing validity or signature."
66+
)
67+
68+
await test_signature(
69+
request.app["tokens"],
70+
signature,
71+
validity + path
72+
)
73+
74+
return await handler(request)

swift_x_account_sharing/middleware.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
"""Middleware for adding CORS headers for API calls."""
22

33

4+
import typing
5+
46
import aiohttp.web
7+
from asyncpg import UniqueViolationError
58

69
from .db import DBConn
710

811

12+
AiohttpHandler = typing.Callable[[aiohttp.web.Request], aiohttp.web.Response]
13+
14+
915
@aiohttp.web.middleware
10-
async def add_cors(request, handler):
16+
async def add_cors(
17+
request: aiohttp.web.Request,
18+
handler: AiohttpHandler
19+
) -> aiohttp.web.Response:
1120
"""Add CORS header for API responses."""
1221
resp = await handler(request)
1322
if "origin" in request.headers.keys():
@@ -16,7 +25,10 @@ async def add_cors(request, handler):
1625

1726

1827
@aiohttp.web.middleware
19-
async def check_db_conn(request, handler):
28+
async def check_db_conn(
29+
request: aiohttp.web.Request,
30+
handler: AiohttpHandler
31+
) -> aiohttp.web.Response:
2032
"""Check if an established database connection exists."""
2133
if (
2234
isinstance(request.app["db_conn"], DBConn)
@@ -26,3 +38,17 @@ async def check_db_conn(request, handler):
2638
reason="No database connection."
2739
)
2840
return await handler(request)
41+
42+
43+
@aiohttp.web.middleware
44+
async def catch_uniqueness_error(
45+
request: aiohttp.web.Request,
46+
handler: AiohttpHandler
47+
) -> aiohttp.web.Response:
48+
"""Catch excepetion arising from a non-unique primary key."""
49+
try:
50+
return await handler(request)
51+
except UniqueViolationError:
52+
raise aiohttp.web.HTTPClientError(
53+
reason="Duplicate entries are not allowed."
54+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Module containing preflight OPTIONS handlers."""
2+
3+
4+
import aiohttp.web
5+
6+
7+
async def handle_delete_preflight(_) -> aiohttp.web.Response:
8+
"""Serve correct response headers to allowed DELETE preflight query."""
9+
resp = aiohttp.web.Response(
10+
headers={
11+
"Access-Control-Allow-Methods": "POST, OPTIONS, DELETE",
12+
"Access-Control-Max-Age": "84600",
13+
}
14+
)
15+
return resp

swift_x_account_sharing/server.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@
2424
from .db import DBConn
2525
from .middleware import (
2626
add_cors,
27-
check_db_conn
27+
check_db_conn,
28+
catch_uniqueness_error
2829
)
30+
from .auth import (
31+
read_in_keys,
32+
handle_validate_authentication,
33+
)
34+
from .preflight import handle_delete_preflight
2935

3036

3137
logging.basicConfig(level=logging.DEBUG)
@@ -34,7 +40,9 @@
3440
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
3541

3642

37-
async def resume_on_start(app):
43+
async def resume_on_start(
44+
app: aiohttp.web.Application
45+
):
3846
"""Resume old instance from start."""
3947
# If using dict_db read the database on disk, if it exists
4048
if (
@@ -46,7 +54,9 @@ async def resume_on_start(app):
4654
await app["db_conn"].open()
4755

4856

49-
async def save_on_shutdown(app):
57+
async def save_on_shutdown(
58+
app: aiohttp.web.Application
59+
):
5060
"""Flush the database on shutdown."""
5161
# If using dict_db dump the database on disk, using default file.
5262
if isinstance(app["db_conn"], InMemDB):
@@ -55,10 +65,15 @@ async def save_on_shutdown(app):
5565
await app["db_conn"].close()
5666

5767

58-
async def init_server():
68+
async def init_server() -> aiohttp.web.Application:
5969
"""Initialize the server."""
6070
app = aiohttp.web.Application(
61-
middlewares=[add_cors, check_db_conn]
71+
middlewares=[
72+
add_cors,
73+
check_db_conn,
74+
handle_validate_authentication,
75+
catch_uniqueness_error,
76+
]
6277
)
6378

6479
if os.environ.get("SHARING_DB_POSTGRES", None):
@@ -75,15 +90,20 @@ async def init_server():
7590
share_container_handler),
7691
aiohttp.web.patch("/share/{owner}/{contanier}", edit_share_handler),
7792
aiohttp.web.delete("/share/{owner}/{container}", delete_share_handler),
93+
aiohttp.web.options("/share/{owner}/{container}",
94+
handle_delete_preflight),
7895
])
7996

8097
app.on_startup.append(resume_on_start)
98+
app.on_startup.append(read_in_keys)
8199
app.on_shutdown.append(save_on_shutdown)
82100

83101
return app
84102

85103

86-
def run_server_devel(app):
104+
def run_server_devel(
105+
app: aiohttp.web.Application
106+
):
87107
"""Run the server in development mode (without HTTPS)."""
88108
aiohttp.web.run_app(
89109
app,

0 commit comments

Comments
 (0)