Skip to content

Commit d084ef0

Browse files
Fix unauthenticated API calls
closes #172 Signed-off-by: Jérôme Jutteau <jerome.jutteau@outscale.com>
1 parent f6fee2a commit d084ef0

File tree

5 files changed

+251
-8
lines changed

5 files changed

+251
-8
lines changed

.github/workflows/pull-request.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ jobs:
5454
OSC_TEST_SECRET_KEY: ${{ secrets.OSC_TEST_SECRET_KEY }}
5555
OSC_TEST_ENDPOINT_ICU: ${{ secrets.OSC_TEST_ENDPOINT_ICU }}
5656
OSC_TEST_ENDPOINT_API: ${{ secrets.OSC_TEST_ENDPOINT_API }}
57+
OSC_TEST_ENDPOINT_FCU: ${{ secrets.OSC_TEST_ENDPOINT_FCU }}
5758
OSC_TEST_REGION: ${{ secrets.OSC_TEST_REGION }}
5859
- name: Test python package building
5960
run: make build
@@ -81,6 +82,7 @@ jobs:
8182
OSC_TEST_SECRET_KEY: ${{ secrets.OSC_TEST_SECRET_KEY }}
8283
OSC_TEST_ENDPOINT_ICU: ${{ secrets.OSC_TEST_ENDPOINT_ICU }}
8384
OSC_TEST_ENDPOINT_API: ${{ secrets.OSC_TEST_ENDPOINT_API }}
85+
OSC_TEST_ENDPOINT_FCU: ${{ secrets.OSC_TEST_ENDPOINT_FCU }}
8486
OSC_TEST_REGION: ${{ secrets.OSC_TEST_REGION }}
8587
- name: Test python package building
8688
run: make build
@@ -108,6 +110,7 @@ jobs:
108110
OSC_TEST_SECRET_KEY: ${{ secrets.OSC_TEST_SECRET_KEY }}
109111
OSC_TEST_ENDPOINT_ICU: ${{ secrets.OSC_TEST_ENDPOINT_ICU }}
110112
OSC_TEST_ENDPOINT_API: ${{ secrets.OSC_TEST_ENDPOINT_API }}
113+
OSC_TEST_ENDPOINT_FCU: ${{ secrets.OSC_TEST_ENDPOINT_FCU }}
111114
OSC_TEST_REGION: ${{ secrets.OSC_TEST_REGION }}
112115
- name: Test python package building
113116
run: make build
@@ -135,6 +138,7 @@ jobs:
135138
OSC_TEST_SECRET_KEY: ${{ secrets.OSC_TEST_SECRET_KEY }}
136139
OSC_TEST_ENDPOINT_ICU: ${{ secrets.OSC_TEST_ENDPOINT_ICU }}
137140
OSC_TEST_ENDPOINT_API: ${{ secrets.OSC_TEST_ENDPOINT_API }}
141+
OSC_TEST_ENDPOINT_FCU: ${{ secrets.OSC_TEST_ENDPOINT_FCU }}
138142
OSC_TEST_REGION: ${{ secrets.OSC_TEST_REGION }}
139143
- name: Test python package building
140144
run: make build
@@ -162,6 +166,7 @@ jobs:
162166
OSC_TEST_SECRET_KEY: ${{ secrets.OSC_TEST_SECRET_KEY }}
163167
OSC_TEST_ENDPOINT_ICU: ${{ secrets.OSC_TEST_ENDPOINT_ICU }}
164168
OSC_TEST_ENDPOINT_API: ${{ secrets.OSC_TEST_ENDPOINT_API }}
169+
OSC_TEST_ENDPOINT_FCU: ${{ secrets.OSC_TEST_ENDPOINT_FCU }}
165170
OSC_TEST_REGION: ${{ secrets.OSC_TEST_REGION }}
166171
- name: Test python package building
167172
run: make build

osc_sdk/sdk.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import urllib.parse
99
from dataclasses import InitVar, dataclass, field
1010
from pathlib import Path
11-
from typing import Any, Dict, List, Mapping, Optional, Tuple, Union, cast
11+
from typing import Any, Dict, List, Mapping, Optional, Set, Tuple, Union, cast
1212

1313
import defusedxml.ElementTree as ET
1414
import fire
@@ -29,7 +29,6 @@
2929
DEFAULT_PROFILE = "default"
3030
DEFAULT_REGION = "eu-west-2"
3131
DEFAULT_VERSION = datetime.date.today().strftime("%Y-%m-%d")
32-
DEFAULT_AUTHENTICATION_METHOD = "accesskey"
3332

3433
DEFAULT_HOST = "outscale.com"
3534

@@ -46,6 +45,31 @@
4645
EncodedCallParameters = Optional[str]
4746
Headers = Tuple[str, str, Dict[str, str]]
4847

48+
NO_AUTH_CALLS: Dict[str, Set[str]] = {
49+
"api": {
50+
"ReadFlexibleGpuCatalog",
51+
"ReadLocations",
52+
"ReadNetAccessPointServices",
53+
"ReadProductTypes",
54+
"ReadPublicIpRanges",
55+
"ReadRegions",
56+
"ReadVmTypes",
57+
"ResetAccountPassword",
58+
"SendResetPasswordEmail",
59+
},
60+
"icu": {
61+
"AuthenticateAccount",
62+
"ResetAccountPassword",
63+
"SendResetPasswordEmail",
64+
"ReadPublicCatalog",
65+
"CheckSignature",
66+
},
67+
"fcu": {
68+
"DescribeRegions",
69+
"ReadPublicIpRanges",
70+
},
71+
}
72+
4973

5074
class Configuration(TypedDict):
5175
method: str
@@ -151,7 +175,7 @@ class ApiCall:
151175
profile: str = DEFAULT_PROFILE
152176
login: Optional[str] = None
153177
password: Optional[str] = None
154-
authentication_method: str = DEFAULT_AUTHENTICATION_METHOD
178+
authentication_method: Optional[str] = None
155179

156180
API_NAME: str = field(default="", init=False)
157181
CONTENT_TYPE = "application/x-www-form-urlencoded"
@@ -179,8 +203,6 @@ def __post_init__(self):
179203
if not self.API_NAME:
180204
raise RuntimeError("API_NAME is required and should not be empty")
181205

182-
self.check_authentication_options()
183-
184206
if self.method not in METHODS_SUPPORTED:
185207
raise Exception(
186208
f"Wrong method {self.method}. Supported: {METHODS_SUPPORTED}."
@@ -202,9 +224,9 @@ def __post_init__(self):
202224
self.endpoint = f"{self.protocol}://{self.endpoint}"
203225

204226
def check_authentication_options(self):
205-
if self.authentication_method not in {"accesskey", "password"}:
227+
if self.authentication_method not in {"accesskey", "password", "none"}:
206228
raise RuntimeError(
207-
"Unsupported authentication method (accesskey or password)"
229+
"Unsupported authentication method (accesskey, password or none)"
208230
)
209231
if self.authentication_method == "accesskey":
210232
if self.access_key is None:
@@ -309,6 +331,14 @@ def make_request(self, call: str, **kwargs: CallParameters):
309331
# Calculate request params
310332
request_params = self.get_parameters(data=kwargs)
311333

334+
if self.authentication_method is None:
335+
if call_need_auth(self.API_NAME, call):
336+
self.authentication_method = "accesskey"
337+
else:
338+
self.authentication_method = "none"
339+
340+
self.check_authentication_options()
341+
312342
if self.authentication_method == "password":
313343
request_params.update(self.get_password_params())
314344

@@ -476,6 +506,14 @@ def make_request(self, call: str, **kwargs: CallParameters):
476506

477507
request_params = self.get_parameters(kwargs, call)
478508

509+
if self.authentication_method is None:
510+
if call_need_auth(self.API_NAME, call):
511+
self.authentication_method = "accesskey"
512+
else:
513+
self.authentication_method = "none"
514+
515+
self.check_authentication_options()
516+
479517
if self.authentication_method == "password":
480518
request_params.update(self.get_password_params())
481519

@@ -675,13 +713,17 @@ def get_conf(profile: str) -> Configuration:
675713
raise RuntimeError(f"Profile {profile} not found in configuration file")
676714

677715

716+
def call_need_auth(service: str, call: str) -> bool:
717+
return call not in NO_AUTH_CALLS.get(service, set())
718+
719+
678720
def api_connect(
679721
service: str,
680722
call: str,
681723
profile: str = DEFAULT_PROFILE,
682724
login: Optional[str] = None,
683725
password: Optional[str] = None,
684-
authentication_method: str = DEFAULT_AUTHENTICATION_METHOD,
726+
authentication_method: Optional[str] = None,
685727
**kwargs: CallParameters,
686728
):
687729
calls = {
@@ -693,6 +735,7 @@ def api_connect(
693735
"lbu": LbuCall,
694736
"okms": OKMSCall,
695737
}
738+
696739
handler = calls[service](
697740
profile, login, password, authentication_method, **get_conf(profile)
698741
)

osc_sdk/test_noauth.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import os
2+
from dataclasses import dataclass
3+
4+
import pytest
5+
6+
from . import sdk
7+
8+
9+
@dataclass
10+
class Env:
11+
access_key: str
12+
secret_key: str
13+
endpoint_icu: str
14+
endpoint_api: str
15+
endpoint_fcu: str
16+
region: str
17+
18+
19+
@pytest.fixture
20+
def env() -> Env:
21+
return Env(
22+
access_key=os.getenv("OSC_TEST_ACCESS_KEY", ""),
23+
secret_key=os.getenv("OSC_TEST_SECRET_KEY", ""),
24+
endpoint_icu=os.getenv("OSC_TEST_ENDPOINT_ICU", ""),
25+
endpoint_api=os.getenv("OSC_TEST_ENDPOINT_API", ""),
26+
endpoint_fcu=os.getenv("OSC_TEST_ENDPOINT_FCU", ""),
27+
region=os.getenv("OSC_TEST_REGION", ""),
28+
)
29+
30+
31+
def test_icu_noauth_call_with_auth_env(env):
32+
icu = sdk.IcuCall(
33+
access_key=env.access_key,
34+
secret_key=env.secret_key,
35+
endpoint=env.endpoint_icu,
36+
region_name=env.region,
37+
)
38+
icu.make_request("ReadPublicCatalog")
39+
assert len(icu.response)
40+
41+
42+
def test_icu_noauth_call_with_empty_auth_env(env):
43+
icu = sdk.IcuCall( # nosec
44+
access_key="",
45+
secret_key="",
46+
endpoint=env.endpoint_icu,
47+
region_name=env.region,
48+
)
49+
icu.make_request("ReadPublicCatalog")
50+
assert len(icu.response)
51+
52+
53+
def test_icu_noauth_basic(env):
54+
icu = sdk.IcuCall(
55+
endpoint=env.endpoint_icu,
56+
region_name=env.region,
57+
)
58+
icu.make_request("ReadPublicCatalog")
59+
assert len(icu.response)
60+
61+
62+
def test_api_noauth_call_with_auth_env(env):
63+
api = sdk.OSCCall(
64+
access_key=env.access_key,
65+
secret_key=env.secret_key,
66+
endpoint=env.endpoint_api,
67+
region_name=env.region,
68+
)
69+
api.make_request("ReadRegions")
70+
assert len(api.response)
71+
72+
73+
def test_api_noauth_call_with_empty_auth_env(env):
74+
api = sdk.OSCCall( # nosec
75+
access_key="",
76+
secret_key="",
77+
endpoint=env.endpoint_api,
78+
region_name=env.region,
79+
)
80+
api.make_request("ReadRegions")
81+
assert len(api.response)
82+
83+
84+
def test_api_noauth_basic(env):
85+
api = sdk.OSCCall(
86+
endpoint=env.endpoint_api,
87+
region_name=env.region,
88+
)
89+
api.make_request("ReadRegions")
90+
assert len(api.response)
91+
92+
93+
def test_fcu_noauth_call_with_auth_env(env):
94+
fcu = sdk.FcuCall(
95+
access_key=env.access_key,
96+
secret_key=env.secret_key,
97+
endpoint=env.endpoint_fcu,
98+
region_name=env.region,
99+
)
100+
fcu.make_request("ReadPublicIpRanges")
101+
assert len(fcu.response)
102+
103+
104+
def test_fcu_noauth_call_with_empty_auth_env(env):
105+
fcu = sdk.FcuCall( # nosec
106+
access_key="",
107+
secret_key="",
108+
endpoint=env.endpoint_fcu,
109+
region_name=env.region,
110+
)
111+
fcu.make_request("ReadPublicIpRanges")
112+
assert len(fcu.response)
113+
114+
115+
def test_fcu_noauth_basic(env):
116+
fcu = sdk.FcuCall(
117+
endpoint=env.endpoint_fcu,
118+
region_name=env.region,
119+
)
120+
fcu.make_request("ReadPublicIpRanges")
121+
assert len(fcu.response)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Assuming you are running this from a prepared virtual environment
5+
PROJECT_ROOT=$(cd "$(dirname $0)/../.." && pwd)
6+
cd $PROJECT_ROOT
7+
c="python osc_sdk/sdk.py"
8+
9+
10+
echo -n "$(basename $0): "
11+
12+
# All calls must fail with a bad auth method even if accesskey method is available
13+
if [ -z "$OSC_TEST_LOGIN" ]; then
14+
echo "error, OSC_TEST_LOGIN must be set"
15+
exit 1
16+
fi
17+
if [ -z "$OSC_TEST_PASSWORD" ]; then
18+
echo "error, OSC_TEST_PASSWORD must be set"
19+
exit 1
20+
fi
21+
22+
# Test password auth
23+
$c icu ListAccessKeys --authentication-method=password --login "$OSC_TEST_LOGIN" --password "$OSC_TEST_PASSWORD" &> /dev/null || { echo "login auth check error 1"; exit 1; }
24+
$c icu ListAccessKeys --authentication-method=password --login "BAD_LOGIN" --password "BAD_PASSWORD" &> /dev/null && { echo "login auth check error 2"; exit 1; }
25+
# Test accesskey auth
26+
$c api ReadVolumes --authentication-method=accesskey &> /dev/null || { echo "accesskey auth check error"; exit 1; }
27+
28+
29+
# On Outscale API, calls which do not require authentication also succeed when authenticated.
30+
$c api ReadRegions --authentication-method=accesskey &> /dev/null || { echo "api:ReadRegion error 1"; exit 1; }
31+
$c api ReadRegions --authentication-method=password --login "$OSC_TEST_LOGIN" --password "$OSC_TEST_PASSWORD" &> /dev/null || { echo "api:ReadRegion error 2"; exit 1; }
32+
$c api ReadRegions --authentication-method=password --login "BAD_LOGIN" --password "BAD_PASSWORD" &> /dev/null || { echo "api:ReadRegion error 3"; exit 1; }
33+
# Bad auth method should still be refused by cli
34+
$c api ReadRegions --authentication-method=bad &> /dev/null && { echo "api:ReadRegion error 4"; exit 1; }
35+
# Explicitly ignore authentication for non auth call
36+
$c api ReadRegions --authentication-method=none &> /dev/null || { echo "api:ReadRegion error 5"; exit 1; }
37+
# Explicitly ignore authentication for auth call
38+
$c api ReadVolumes --authentication-method=none &> /dev/null && { echo "api:ReadVolumes error 6"; exit 1; }
39+
# Should default to authentication-method=none
40+
$c api ReadRegions &> /dev/null || { echo "api:ReadRegion error 7"; exit 1; }
41+
42+
# On ICU, calls which do not require authentication also succeed when authenticated.
43+
$c icu ReadPublicCatalog --authentication-method=accesskey &> /dev/null || { echo "icu:ReadPublicCatalog error 1"; exit 1; }
44+
$c icu ReadPublicCatalog --authentication-method=password --login "$OSC_TEST_LOGIN" --password "$OSC_TEST_PASSWORD" &> /dev/null || { echo "icu:ReadPublicCatalog error 2"; exit 1; }
45+
$c icu ReadPublicCatalog --authentication-method=password --login "BAD_LOGIN" --password "BAD_PASSWORD" &> /dev/null || { echo "icu:ReadPublicCatalog error 3"; exit 1; }
46+
# Bad auth method should still be refused by cli
47+
$c icu ReadPublicCatalog --authentication-method=bad &> /dev/null && { echo "icu:ReadPublicCatalog error 4"; exit 1; }
48+
# Explicitly ignore authentication for non auth call
49+
$c icu ReadPublicCatalog --authentication-method=none &> /dev/null || { echo "icu:ReadPublicCatalog error 5"; exit 1; }
50+
# Explicitly ignore authentication for auth call
51+
$c icu GetAccount --authentication-method=none &> /dev/null && { echo "icu:GetAccount error 6"; exit 1; }
52+
# Should default to authentication-method=none
53+
$c icu ReadPublicCatalog &> /dev/null || { echo "icu:ReadPublicCatalog error 7"; exit 1; }
54+
55+
# On FCU, calls which do not require authentication also succeed when authenticated.
56+
$c fcu DescribeRegions --authentication-method=accesskey &> /dev/null || { echo "fcu:DescribeRegions error 1"; exit 1; }
57+
# On FCU, this kind call should not work with password authentication.
58+
$c fcu DescribeRegions --authentication-method=password --login "$OSC_TEST_LOGIN" --password "$OSC_TEST_PASSWORD" &> /dev/null && { echo "fcu:DescribeRegions error 2"; exit 1; }
59+
$c fcu DescribeRegions --authentication-method=password --login "BAD_LOGIN" --password "BAD_PASSWORD" &> /dev/null && { echo "fcu:DescribeRegions error 3"; exit 1; }
60+
# Bad auth method should still be refused by cli
61+
$c fcu DescribeRegions --authentication-method=bad &> /dev/null && { echo "fcu:DescribeRegions error 4"; exit 1; }
62+
# Explicitly ignore authentication for non auth call
63+
$c fcu DescribeRegions --authentication-method=none &> /dev/null || { echo "fcu:DescribeRegions error 5"; exit 1; }
64+
# Explicitly ignore authentication for auth call
65+
$c fcu DescribeVolumes --authentication-method=none &> /dev/null && { echo "fcu:DescribeVolumes error 6"; exit 1; }
66+
# Should default to authentication-method=none
67+
$c fcu DescribeRegions &> /dev/null || { echo "fcu:DescribeRegions error 7"; exit 1; }
68+
69+
echo "OK"

tests/test_pytest.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ if [ -z "$OSC_TEST_ENDPOINT_ICU" ]; then
1717
exit 1
1818
fi
1919

20+
if [ -z "$OSC_TEST_ENDPOINT_FCU" ]; then
21+
echo "OSC_TEST_ENDPOINT_FCU not set, aborting"
22+
exit 1
23+
fi
24+
2025
if [ -z "$OSC_TEST_REGION" ]; then
2126
echo "OSC_TEST_REGION not set, aborting"
2227
exit 1

0 commit comments

Comments
 (0)