From 5aca28b3636bbc6a6968249f5e4be11a11b203bc Mon Sep 17 00:00:00 2001 From: Jakub Schier Date: Wed, 2 Aug 2023 10:47:20 +0200 Subject: [PATCH 1/6] enable ssl cert and api key option for es batman --- seacatauth/batman/elk.py | 63 +++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/seacatauth/batman/elk.py b/seacatauth/batman/elk.py index ead13d32..e8baf0c1 100644 --- a/seacatauth/batman/elk.py +++ b/seacatauth/batman/elk.py @@ -1,4 +1,5 @@ import re +import ssl import logging import typing import aiohttp @@ -26,8 +27,13 @@ class ELKIntegration(asab.config.Configurable): ConfigDefaults = { "url": "http://localhost:9200/", - "username": "elastic", - "password": "elastic", + # Credentials/api key (mutualy exclusive) + "username": "", + "password": "", + "api_key": "", + + # Certs + "ca_file": "", # List of elasticsearch system users # If Seacat Auth has users with one of these usernames, it will not sync them @@ -59,6 +65,21 @@ def __init__(self, batman_svc, config_section_name="batman:elk", config=None): else: self.Authorization = None + api_key = self.Config.get("api_key") + self.Headers = None + if api_key != "": + self.Headers = { + "Authorization": "ApiKey {}".format(api_key) + } + + # Prep for SSL + ca_cert = self.Config.get("ca_file") + self.SSLContext = None + if ca_cert != "": + self.SSLContext = ssl.create_default_context(cafile=ca_cert) + self.SSLContext.check_hostname = True + self.SSLContext.verify_mode = ssl.CERT_REQUIRED + self.URL = self.Config.get("url").rstrip("/") self.ResourcePrefix = self.Config.get("resource_prefix") self.ELKResourceRegex = re.compile("^{}".format( @@ -88,13 +109,14 @@ async def _initialize_resources(self): """ # Fetch ELK roles try: - async with aiohttp.ClientSession(auth=self.Authorization) as session: - async with session.get("{}/_xpack/security/role".format(self.URL)) as resp: - if resp.status != 200: - text = await resp.text() - L.error("Failed to fetch ElasticSearch roles:\n{}".format(text[:1000])) - return - elk_roles_data = await resp.json() + async with aiohttp.TCPConnector(ssl=self.SSLContext or False) as conn: + async with aiohttp.ClientSession(connector=conn, auth=self.Authorization, headers=self.Headers) as session: + async with session.get("{}/_xpack/security/role".format(self.URL)) as resp: + if resp.status != 200: + text = await resp.text() + L.error("Failed to fetch ElasticSearch roles:\n{}".format(text[:1000])) + return + elk_roles_data = await resp.json() except Exception as e: L.error("Communication with ElasticSearch produced {}: {}".format(type(e).__name__, str(e))) return @@ -179,17 +201,18 @@ async def sync(self, cred: dict, elk_resources: typing.Iterable): json["roles"] = list(elk_roles) try: - async with aiohttp.ClientSession(auth=self.Authorization) as session: - async with session.post("{}/_xpack/security/user/{}".format(self.URL, username), json=json) as resp: - if resp.status == 200: - # Everything is alright here - pass - else: - text = await resp.text() - L.warning( - "Failed to create/update user in ElasticSearch:\n{}".format(text[:1000]), - struct_data={"cid": cred["_id"]} - ) + async with aiohttp.TCPConnector(ssl=self.SSLContext) as conn: + async with aiohttp.ClientSession(connector=conn, auth=self.Authorization, headers=self.Headers) as session: + async with session.post("{}/_xpack/security/user/{}".format(self.URL, username), json=json) as resp: + if resp.status == 200: + # Everything is alright here + pass + else: + text = await resp.text() + L.warning( + "Failed to create/update user in ElasticSearch:\n{}".format(text[:1000]), + struct_data={"cid": cred["_id"]} + ) except Exception as e: L.error( "Communication with ElasticSearch produced {}: {}".format(type(e).__name__, str(e)), From 2f75a4ec631a821a145ee50a5e574871a71cbbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Wed, 2 Aug 2023 16:58:19 +0200 Subject: [PATCH 2/6] Basic client reference docs --- docs/reference/clients.md | 124 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/reference/clients.md diff --git a/docs/reference/clients.md b/docs/reference/clients.md new file mode 100644 index 00000000..967c3f0b --- /dev/null +++ b/docs/reference/clients.md @@ -0,0 +1,124 @@ +--- +title: Clients +--- + +# Clients + +A Client is an entity that uses authentication and authorization services provided by the Authorization Server. +This authorization enables the Client to access protected resources. +In a common scenario, the Client is a browser application and the Resource Owner is a backend application +located on a remote server. + +Before a Client can ask for authorization, it must get registered at the Authorization Server and obtain a unique ID. +The registration can either be done in Admin UI or via the Admin API. + +## Client metadata + +The actual list of client metadata supported by Seacat Auth can be obtained at `GET /client/features` API. + +### Canonical OAuth 2.0 and OpenID Connect metadata + +Defined in [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html) +and [OAuth 2.0 Dynamic Client Registration Protocol](https://www.rfc-editor.org/rfc/rfc7591#section-2). + +#### `client_id` +`NOT EDITABLE` Unique ID of the Client. By default, it is an opaque string generated by the Authorization Server. +It is possible to request a custom ID by supplying the non-canonical `preferred_client_id` parameter in the +client registration request. +The ID is not editable once the client is already registered. + +#### `client_name` +`REQUIRED` Human-palatable name of the Client to be presented to the End-User. + +#### `client_secret` +OAuth 2.0 client secret string. This value is used by confidential clients to authenticate to the token endpoint. +It is generated by the Authorization Server and is not directly editable. + +#### `client_uri` +URL of the home page of the Client. + +#### `redirect_uris` +`REQUIRED` Array of Redirection URI values used by the Client. + +#### `application_type` +Kind of the application. The default, if omitted, is `web`. + +Supported options: `web` + +#### `response_types` +JSON array containing a list of the OAuth 2.0 response_type values that the Client is declaring that it will restrict +itself to using. If omitted, the default is that the Client will use only the `code` Response Type. + +Supported options: `code` + +#### `grant_types` +JSON array containing a list of the OAuth 2.0 Grant Types that the Client is declaring that it will restrict itself +to using. If omitted, the default is that the Client will use only the `authorization_code` Grant Type. + +Supported options: `authorization_code` + +#### `token_endpoint_auth_method` +Requested Client Authentication method for the Token Endpoint. If omitted, the default is `none`. + +Supported options: `none` + +#### `code_challenge_method` +Code Challenge Method (used in [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636)) +that the Client will restrict itself to use at the Authorize Endpoint. The default, if omitted, is `none`. + +Supported options: + +- `none`: PKCE is disabled. +- `plain`: Code challenge is the same as the code verifier. +- `S256`: Code challenge is derived from the code verifier by using SHA-256 hashing algorithm. + + +## Non-canonical metadata + +These are Seacat-Auth-specific features. + +#### `preferred_client_id` +`REGISTRATION ONLY` Requests a specific `client_id` instead of a randomly generated one at client registration. + +#### `redirect_uri_validation_method` +Specifies the method how the redirect URI used in authorization requests is validated. The default and recommended +value is `full_match`. + +Supported options: + +- `full_match`: **The only OAuth2.0 compliant option.** Requested Redirect URI must exactly match one of the registered + Redirect URIs. +- `prefix_match`: Requested Redirect URI must start with one of the registered Redirect URIs and their hostname must + exactly match. +- `none`: There is no Redirect URI validation. Not secure. + +#### `cookie_name` +`NOT EDITABLE` Unique cookie name derived from Client ID by the Authorization Server. Cookie with this name holds +the information + +#### `cookie_webhook_uri` +Webhook URI for setting additional custom cookies at the cookie entrypoint. It must be a back-channel URI and must +accept a JSON PUT request and respond with a JSON object of cookies to set. + +#### `cookie_entry_uri` +Public URI of the client's cookie entrypoint. This field is **required** for cookie-based authorization (including +batman authorization). One such entrypoint should be available on every hostname where there are Clients that use +cookie-based authorization. + +#### `cookie_domain` +Domain of the client cookie. If not specified, the application's default cookie domain is used. + +#### `authorize_uri` +URL of OAuth authorize endpoint. Useful when logging in from different than the default domain. + +#### `login_uri` +URL of preferred login page. Useful when logging in from different than the default domain. + +#### `authorize_anonymous_users` +Boolean value specifying whether to authorize requests with anonymous users. + +#### `anonymous_cid` +ID of credentials that is used for authenticating anonymous sessions. + +#### `session_expiration` +Client session expiration. The value can be either the number of seconds or a time-unit string such as `4 h` or `3 d`. From 54aa050638c6f4e3730ef9f361e97b8358837494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Thu, 3 Aug 2023 13:19:41 +0200 Subject: [PATCH 3/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39635ae..23a7b45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Features - Seacat Auth listens on ports 3081 and 8900 by default (#230, PLUM Sprint 230714) +- Add SSL and API key support in ELK batman (#241, GREY Sprint 230714) --- From 0ccc50f4e8442a7c3095be4b5213b4cca990df2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Thu, 3 Aug 2023 13:36:44 +0200 Subject: [PATCH 4/6] Ensure mutual exclusivity of basic and apikey auth --- seacatauth/batman/elk.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/seacatauth/batman/elk.py b/seacatauth/batman/elk.py index e8baf0c1..d73e7bfe 100644 --- a/seacatauth/batman/elk.py +++ b/seacatauth/batman/elk.py @@ -60,17 +60,19 @@ def __init__(self, batman_svc, config_section_name="batman:elk", config=None): username = self.Config.get("username") password = self.Config.get("password") - if username != "": - self.Authorization = aiohttp.BasicAuth(username, password) - else: - self.Authorization = None - api_key = self.Config.get("api_key") - self.Headers = None - if api_key != "": + if username != "" and api_key != "": + raise ValueError("Cannot authenticate with both 'api_key' and 'username'+'password'.") + if username != "": + self.Headers = { + "Authorization": aiohttp.BasicAuth(username, password).encode() + } + elif api_key != "": self.Headers = { "Authorization": "ApiKey {}".format(api_key) } + else: + self.Headers = None # Prep for SSL ca_cert = self.Config.get("ca_file") @@ -110,7 +112,7 @@ async def _initialize_resources(self): # Fetch ELK roles try: async with aiohttp.TCPConnector(ssl=self.SSLContext or False) as conn: - async with aiohttp.ClientSession(connector=conn, auth=self.Authorization, headers=self.Headers) as session: + async with aiohttp.ClientSession(connector=conn, headers=self.Headers) as session: async with session.get("{}/_xpack/security/role".format(self.URL)) as resp: if resp.status != 200: text = await resp.text() @@ -202,7 +204,7 @@ async def sync(self, cred: dict, elk_resources: typing.Iterable): try: async with aiohttp.TCPConnector(ssl=self.SSLContext) as conn: - async with aiohttp.ClientSession(connector=conn, auth=self.Authorization, headers=self.Headers) as session: + async with aiohttp.ClientSession(connector=conn, headers=self.Headers) as session: async with session.post("{}/_xpack/security/user/{}".format(self.URL, username), json=json) as resp: if resp.status == 200: # Everything is alright here From 5fafb14d87635c0facba980569e968f15730cc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= <11579460+byewokko@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:57:11 +0200 Subject: [PATCH 5/6] Update test dependencies because of #232 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ce1d322..352d42d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,7 @@ jobs: python -m pip install --upgrade pip pip install bson pip install pymongo + pip install jwcrypto pip install git+https://github.com/TeskaLabs/asab.git#egg=asab[encryption] - name: Test with unittest From 4a92e08aa03c3aaf0297de6084ee9e2e44643f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Hru=C5=A1ka?= Date: Thu, 3 Aug 2023 16:04:01 +0200 Subject: [PATCH 6/6] Update references to obsolete classes --- seacatauth/communication/abc.py | 2 +- seacatauth/communication/builders/abc.py | 2 +- seacatauth/credentials/providers/abc.py | 2 +- seacatauth/external_login/providers/generic.py | 2 +- seacatauth/tenant/providers/abc.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/seacatauth/communication/abc.py b/seacatauth/communication/abc.py index 8dd36165..75e27c55 100644 --- a/seacatauth/communication/abc.py +++ b/seacatauth/communication/abc.py @@ -10,7 +10,7 @@ # -class CommunicationProviderABC(asab.ConfigObject, abc.ABC): +class CommunicationProviderABC(asab.Configurable, abc.ABC): Channel = None diff --git a/seacatauth/communication/builders/abc.py b/seacatauth/communication/builders/abc.py index aa488bca..38a3037a 100644 --- a/seacatauth/communication/builders/abc.py +++ b/seacatauth/communication/builders/abc.py @@ -12,7 +12,7 @@ # -class MessageBuilderABC(asab.ConfigObject, abc.ABC): +class MessageBuilderABC(asab.Configurable, abc.ABC): """ Constructs a message object (dictionary) """ diff --git a/seacatauth/credentials/providers/abc.py b/seacatauth/credentials/providers/abc.py index 201101dc..50f6f2fb 100644 --- a/seacatauth/credentials/providers/abc.py +++ b/seacatauth/credentials/providers/abc.py @@ -11,7 +11,7 @@ # -class CredentialsProviderABC(asab.ConfigObject, abc.ABC): +class CredentialsProviderABC(asab.Configurable, abc.ABC): Type = "abc" Editable = False diff --git a/seacatauth/external_login/providers/generic.py b/seacatauth/external_login/providers/generic.py index 72a50f11..e8e3e819 100644 --- a/seacatauth/external_login/providers/generic.py +++ b/seacatauth/external_login/providers/generic.py @@ -16,7 +16,7 @@ # -class GenericOAuth2Login(asab.ConfigObject): +class GenericOAuth2Login(asab.Configurable): """ Generic OAuth2 login provider diff --git a/seacatauth/tenant/providers/abc.py b/seacatauth/tenant/providers/abc.py index fb762291..deafba2e 100644 --- a/seacatauth/tenant/providers/abc.py +++ b/seacatauth/tenant/providers/abc.py @@ -4,7 +4,7 @@ import asab -class TenantsProviderABC(asab.ConfigObject, abc.ABC): +class TenantsProviderABC(asab.Configurable, abc.ABC): def __init__(self, provider_id, config_section_name, config=None):