Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0df9a4d
adjust session keywords and tests to support secrets
oboehmer Oct 28, 2025
7b49871
temp upgrade of robot in pipeline
oboehmer Oct 28, 2025
663f6da
fix custom auth, make it more concicse
oboehmer Oct 28, 2025
c8c6d28
add missing secretvar.py
oboehmer Oct 28, 2025
7fdb7f0
remove comments
oboehmer Oct 28, 2025
11f8f19
test with different robot versions
oboehmer Oct 28, 2025
fef032b
add Secret logic also to all other keywords which support auth
oboehmer Oct 28, 2025
2827af7
use skipif
oboehmer Oct 28, 2025
affb0b7
shorten docstring
oboehmer Oct 28, 2025
a25afab
fix some flake8 errors
oboehmer Oct 28, 2025
f90a136
simplify logic
oboehmer Oct 28, 2025
c6b7e85
add comment on robot version
oboehmer Oct 28, 2025
b0b5d6d
fix artificat pipeline error
oboehmer Oct 28, 2025
970acc8
trigger pipeline
oboehmer Oct 29, 2025
011d534
move unit tests to utests/test_utils.py
oboehmer Nov 18, 2025
b407196
remove imports no longer required
oboehmer Nov 18, 2025
bd4c74c
add line
oboehmer Nov 18, 2025
ffa89ec
reference released 7.4 in pipeline
oboehmer Dec 12, 2025
cc9df64
Merge branch 'master' into secret-support
oboehmer Dec 17, 2025
abae2e7
adjust auth tests to local http server
oboehmer Dec 17, 2025
fd38b06
use flag to determine if secrets are supported
oboehmer Dec 17, 2025
74d0b12
prevent secrets to be logged, even in TRACE/DEBUG log level
oboehmer Dec 24, 2025
de8ebf6
use different password value for secret passwrds
oboehmer Dec 25, 2025
d8ddff8
mask Authorization header in HTTP debug output when secrets are used
oboehmer Dec 25, 2025
7e96c48
adjust unit test
oboehmer Dec 25, 2025
9907dec
remove unneeded functions
oboehmer Dec 25, 2025
15298bf
remove redundant comments
oboehmer Dec 25, 2025
4d726b0
replace session attribute with helper function and tracking dict
oboehmer Dec 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
python-version: [ 3.8, 3.12 ]
# test with robot without and with Secret support? Not sure if
# it is worth it?
robot-version: [ 7.3.2, 7.4 ]
steps:
- uses: actions/checkout@v4
- name: Set up Python
Expand All @@ -23,6 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -e .[test]
python -m pip install robotframework==${{ matrix.robot-version }}
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
Expand Down Expand Up @@ -59,5 +63,5 @@ jobs:
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: rf-tests-report-${{ matrix.os }}-${{ matrix.python-version }}
name: rf-tests-report-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.robot-version }}
path: ./tests-report
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ env/*

# ignore http server log
atests/http_server/http_server.log
.claude/
7 changes: 7 additions & 0 deletions atests/secretvar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# inject secret into robot suite.. doing this via python
# to ensure this can also run in older robot versions
try:
from robot.api.types import Secret
SECRET_PASSWORD = Secret("secret_passwd")
except (ImportError, ModuleNotFoundError):
SECRET_PASSWORD = "not-supported"
39 changes: 36 additions & 3 deletions atests/test_authentication.robot
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Library RequestsLibrary
Library customAuthenticator.py
Resource res_setup.robot

Variables secretvar.py

*** Test Cases ***
Get With Auth
Expand All @@ -23,12 +23,45 @@ Get With Custom Auth

Get With Digest Auth
[Tags] get get-cert
${auth}= Create List user pass
${auth}= Create List user passwd
Create Digest Session
... authsession
... ${HTTP_LOCAL_SERVER}
... auth=${auth}
... debug=3
${resp}= GET On Session authsession /digest-auth/auth/user/pass
${resp}= GET On Session authsession /digest-auth/auth/user/passwd
Should Be Equal As Strings ${resp.status_code} 200
Should Be Equal As Strings ${resp.json()['authenticated']} True

Get With Auth with Robot Secrets
[Tags] robot-74 get get-cert
Skip If $SECRET_PASSWORD == "not-supported"
... msg=robot version does not support secrets
${auth}= Create List user ${SECRET_PASSWORD}
Create Session authsession ${HTTP_LOCAL_SERVER} auth=${auth}
${resp}= GET On Session authsession /basic-auth/user/secret_passwd
Should Be Equal As Strings ${resp.status_code} 200
Should Be Equal As Strings ${resp.json()['authenticated']} True

Get With Digest Auth with Robot Secrets
[Tags] robot-74 get get-cert
Skip If $SECRET_PASSWORD == "not-supported"
... msg=robot version does not support secrets
${auth}= Create List user ${SECRET_PASSWORD}
Create Digest Session
... authsession
... ${HTTP_LOCAL_SERVER}
... auth=${auth}
... debug=3
${resp}= GET On Session authsession /digest-auth/auth/user/secret_passwd
Should Be Equal As Strings ${resp.status_code} 200
Should Be Equal As Strings ${resp.json()['authenticated']} True

Session-less GET With Auth with Robot Secrets
[Tags] robot-74 get get-cert session-less
Skip If $SECRET_PASSWORD == "not-supported"
... msg=robot version does not support secrets
${auth}= Create List user ${SECRET_PASSWORD}
${resp}= GET ${HTTP_LOCAL_SERVER}/basic-auth/user/secret_passwd auth=${auth}
Should Be Equal As Strings ${resp.status_code} 200
Should Be Equal As Strings ${resp.json()['authenticated']} True
30 changes: 26 additions & 4 deletions src/RequestsLibrary/RequestsKeywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from RequestsLibrary import log
from RequestsLibrary.compat import urljoin
from RequestsLibrary.utils import (
check_and_process_secrets,
is_list_or_tuple,
is_file_descriptor,
warn_if_equal_symbol_in_url_session_less,
Expand All @@ -22,6 +23,14 @@ def __init__(self):
self.timeout = None
self.cookies = None
self.last_response = None
self._request_has_secrets = False
self._session_secrets = {} # Maps session object ID to secrets flag

def _get_session_secrets_flag(self, session):
"""Get the secrets flag for a session object"""
if not session:
return False
return self._session_secrets.get(id(session), False)

def _common_request(self, method, session, uri, **kwargs):

Expand All @@ -30,6 +39,19 @@ def _common_request(self, method, session, uri, **kwargs):
else:
request_function = getattr(requests, "request")

auth = kwargs.get("auth")
if auth is not None and isinstance(auth, (list, tuple)):
kwargs["auth"], contains_secrets = check_and_process_secrets(auth)
else:
contains_secrets = False

if session:
# Check if the session was created with robot secrets
contains_secrets = contains_secrets or self._get_session_secrets_flag(session)

# Store secrets flag for _print_debug to access
self._request_has_secrets = contains_secrets

self._capture_output()

resp = request_function(
Expand All @@ -40,7 +62,7 @@ def _common_request(self, method, session, uri, **kwargs):
**kwargs
)

log.log_request(resp)
log.log_request(resp, has_secrets=contains_secrets)
self._print_debug()

log.log_response(resp)
Expand All @@ -59,7 +81,7 @@ def _close_file_descriptors(files, data):
"""
Helper method that closes any open file descriptors.
"""

if is_list_or_tuple(files):
files_descriptor_to_close = filter(
is_file_descriptor, [file[1][1] for file in files] + [data]
Expand All @@ -68,10 +90,10 @@ def _close_file_descriptors(files, data):
files_descriptor_to_close = filter(
is_file_descriptor, list(files.values()) + [data]
)

for file_descriptor in files_descriptor_to_close:
file_descriptor.close()

@staticmethod
def _merge_url(session, uri):
"""
Expand Down
53 changes: 45 additions & 8 deletions src/RequestsLibrary/SessionKeywords.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
import sys

import requests
Expand All @@ -12,7 +13,8 @@
from RequestsLibrary import utils
from RequestsLibrary.compat import RetryAdapter, httplib
from RequestsLibrary.exceptions import InvalidExpectedStatus, InvalidResponse
from RequestsLibrary.utils import is_string_type
from RequestsLibrary.log import AUTHORIZATION
from RequestsLibrary.utils import is_string_type, check_and_process_secrets

from .RequestsKeywords import RequestsKeywords

Expand All @@ -25,6 +27,10 @@
class SessionKeywords(RequestsKeywords):
DEFAULT_RETRY_METHOD_LIST = RetryAdapter.get_default_allowed_methods()

def _set_session_secrets_flag(self, session, has_secrets):
"""Store the secrets flag for a session object using its id as key"""
self._session_secrets[id(session)] = has_secrets

def _create_session(
self,
alias,
Expand Down Expand Up @@ -172,15 +178,19 @@ def create_session(
Note that max_retries must be greater than 0.

"""
auth = requests.auth.HTTPBasicAuth(*auth) if auth else None
if auth:
processed_auth, session_has_secrets = check_and_process_secrets(auth)
auth = requests.auth.HTTPBasicAuth(*processed_auth)
else:
session_has_secrets = False

logger.info(
"Creating Session using : alias=%s, url=%s, headers=%s, \
cookies=%s, auth=%s, timeout=%s, proxies=%s, verify=%s, \
debug=%s "
% (alias, url, headers, cookies, auth, timeout, proxies, verify, debug)
)
return self._create_session(
session = self._create_session(
alias=alias,
url=url,
headers=headers,
Expand All @@ -196,6 +206,8 @@ def create_session(
retry_status_list=retry_status_list,
retry_method_list=retry_method_list,
)
self._set_session_secrets_flag(session, session_has_secrets)
return session

@keyword("Create Client Cert Session")
def create_client_cert_session(
Expand Down Expand Up @@ -262,7 +274,11 @@ def create_client_cert_session(
eg. set to [502, 503] to retry requests if those status are returned.
Note that max_retries must be greater than 0.
"""
auth = requests.auth.HTTPBasicAuth(*auth) if auth else None
if auth:
processed_auth, session_has_secrets = check_and_process_secrets(auth)
auth = requests.auth.HTTPBasicAuth(*processed_auth)
else:
session_has_secrets = False

logger.info(
"Creating Session using : alias=%s, url=%s, headers=%s, \
Expand Down Expand Up @@ -300,6 +316,7 @@ def create_client_cert_session(
)

session.cert = tuple(client_certs)
self._set_session_secrets_flag(session, session_has_secrets)
return session

@keyword("Create Custom Session")
Expand Down Expand Up @@ -452,9 +469,14 @@ def create_digest_session(
eg. set to [502, 503] to retry requests if those status are returned.
Note that max_retries must be greater than 0.
"""
digest_auth = requests.auth.HTTPDigestAuth(*auth) if auth else None
if auth:
processed_auth, session_has_secrets = check_and_process_secrets(auth)
digest_auth = requests.auth.HTTPDigestAuth(*processed_auth)
else:
digest_auth = None
session_has_secrets = False

return self._create_session(
session = self._create_session(
alias=alias,
url=url,
headers=headers,
Expand All @@ -470,6 +492,8 @@ def create_digest_session(
retry_status_list=retry_status_list,
retry_method_list=retry_method_list,
)
self._set_session_secrets_flag(session, session_has_secrets)
return session

@keyword("Create Ntlm Session")
def create_ntlm_session(
Expand Down Expand Up @@ -543,7 +567,8 @@ def create_ntlm_session(
" - expected 3, got {}".format(len(auth))
)
else:
ntlm_auth = HttpNtlmAuth("{}\\{}".format(auth[0], auth[1]), auth[2])
processed_auth, session_has_secrets = check_and_process_secrets(auth)
ntlm_auth = HttpNtlmAuth("{}\\{}".format(processed_auth[0], processed_auth[1]), processed_auth[2])
logger.info(
"Creating NTLM Session using : alias=%s, url=%s, \
headers=%s, cookies=%s, ntlm_auth=%s, timeout=%s, \
Expand All @@ -561,7 +586,7 @@ def create_ntlm_session(
)
)

return self._create_session(
session = self._create_session(
alias=alias,
url=url,
headers=headers,
Expand All @@ -577,6 +602,8 @@ def create_ntlm_session(
retry_status_list=retry_status_list,
retry_method_list=retry_method_list,
)
self._set_session_secrets_flag(session, session_has_secrets)
return session

@keyword("Session Exists")
def session_exists(self, alias):
Expand Down Expand Up @@ -656,4 +683,14 @@ def _print_debug(self):
debug_info = "\n".join(
[ll.rstrip() for ll in debug_info.splitlines() if ll.strip()]
)

# Mask Authorization header in debug output when secrets are used
if self._request_has_secrets:
debug_info = re.sub(
rf'({AUTHORIZATION}:)\s*([^\n]+)',
r'\1 *****',
debug_info,
flags=re.IGNORECASE
)

logger.debug(debug_info)
11 changes: 8 additions & 3 deletions src/RequestsLibrary/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ def log_response(response):
)


def log_request(response):
def log_request(response, has_secrets=False):
request = response.request
if response.history:
original_request = response.history[0].request
redirected = "(redirected) "
else:
original_request = request
redirected = ""

# Mask Authorization header based on whether secrets were used
safe_headers = dict(original_request.headers)
if logger.LOGLEVEL not in ['TRACE', 'DEBUG'] and AUTHORIZATION in safe_headers:
safe_headers[AUTHORIZATION] = '*****'
if AUTHORIZATION in safe_headers:
# If secrets were used, always mask. Otherwise, only mask if not in DEBUG/TRACE
if has_secrets or logger.LOGLEVEL not in ['TRACE', 'DEBUG']:
safe_headers[AUTHORIZATION] = '*****'

logger.info(
"%s Request : " % original_request.method.upper()
+ "url=%s %s\n " % (original_request.url, redirected)
Expand Down
31 changes: 31 additions & 0 deletions src/RequestsLibrary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from requests.status_codes import codes
from requests.structures import CaseInsensitiveDict
from robot.api import logger
try:
from robot.api.types import Secret
robot_supports_secrets = True
except (ImportError, ModuleNotFoundError):
robot_supports_secrets = False

from RequestsLibrary.compat import urlencode
from RequestsLibrary.exceptions import UnknownStatusError
Expand Down Expand Up @@ -74,9 +79,35 @@ def is_string_type(data):
def is_file_descriptor(fd):
return isinstance(fd, io.IOBase)


def is_list_or_tuple(data):
return isinstance(data, (list, tuple))


def check_and_process_secrets(auth):
"""
Check if auth contains secrets and process them

Returns:
tuple: (processed_auth, has_secrets_flag)
"""
if not auth or not isinstance(auth, (list, tuple)):
return auth, False

if robot_supports_secrets:
has_secrets_flag = False
processed = []
for a in auth:
if isinstance(a, Secret):
has_secrets_flag = True
processed.append(a.value)
else:
processed.append(a)
return tuple(processed), has_secrets_flag
else:
return auth, False


def utf8_urlencode(data):
if is_string_type(data):
return data.encode("utf-8")
Expand Down
Loading
Loading