Repository: ansible-collections/community.general

Issue:
Redfish Session Auth with Dell iDRAC9
### Summary

When attempting to create a Redfish Session as documented here: https://github.com/ansible-collections/community.general/blob/fe520a6b09aeab283f1477556b49cb64386a8e10/plugins/modules/redfish_command.py#L874-L875

Dell iDRAC9's (version 5.x or 6.x, does not appear to impact iDRAC8) return an error:

```
fatal: [x.x.x.x]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "msg": "HTTP Error 415 on POST request to 'https://REDACTED/redfish/v1/SessionService/Sessions', extended message: 'A general error has occurred. See Resolution for information on how to resolve the error.'"}
```

Testing manually (outside of Ansible) also returns an error:

```
{
    "error": {
        "code": "Base.1.5.GeneralError",
        "message": "A general error has occurred. See ExtendedInfo for more information.",
        "@Message.ExtendedInfo": {
            "@odata.type": "#Message.v1_0_0.Message",
            "MessageId": "Base.1.5.GeneralError",
            "Message": "A general error has occurred. See Resolution for information on how to resolve the error.",
            "Resolution": "Redfish request contains unsupported media type. Correct the request body and resubmit.",
            "Severity": "Warning"
        }
    }
}
```

This appears to stem from the ansible module setting `force_basic_auth = True` as specified here: https://github.com/ansible-collections/community.general/blob/a7783c48ff7d488a94e7e2f7e78a1364af25783a/plugins/module_utils/redfish_utils.py#L60

iDRAC9's for unknown reasons will fail the session creation if basic_auth is enabled for the POST request. Ideally Dell would handle that differently, but is it possible to disable that flag for just the session creation task?

### Issue Type

Bug Report

### Component Name

redfish_utils.py

### Ansible Version

Tested with:

```
$ ansible --version

ansible [core 2.13.3]
  config file = /.ansible.cfg
  configured module search path = ['.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = .local/share/virtualenvs/ansitest-cGU-0QPt/lib/python3.8/site-packages/ansible
  ansible collection location = /GitHub/ansible/collections
  executable location = .local/share/virtualenvs/ansitest-cGU-0QPt/bin/ansible
  python version = 3.8.14 (default, Oct 14 2022, 09:34:47) [Clang 14.0.0 (clang-1400.0.29.102)]
  jinja version = 3.1.2
  libyaml = False
```

and

```
ansible 2.10.17
  config file = /Users/sseekamp/GitHub/firmware_updater/ansible.cfg
  configured module search path = ['.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location =.local/share/virtualenvs/firmware_updater-aX3uA6gG/lib/python3.8/site-packages/ansible
  executable location = .local/share/virtualenvs/firmware_updater-aX3uA6gG/bin/ansible
  python version = 3.8.14 (default, Oct 14 2022, 09:34:47) [Clang 14.0.0 (clang-1400.0.29.102)]
```


### Community.general Version

Tested with:

```
$ ansible-galaxy collection list community.general

# GitHub/ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 6.2.0  

# .local/share/virtualenvs/ansitest-cGU-0QPt/lib/python3.8/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.1.0  

```


### Configuration

```
$ ansible-config dump --only-changed

COLLECTIONS_PATHS(.ansible.cfg) = ['GitHub/ansible/collections']
```


### OS / Environment

OSX 13.1
Ubuntu Focal

### Steps to Reproduce

```
---
- name: Session test
  gather_facts: false
  hosts: localhost

  tasks:
  - name: Create Redfish session
    community.general.redfish_command:
      category: Sessions
      command: CreateSession
      baseuri: "{{ bmc_address }}"
      username: "{{ bmc_username }}"
      password: "{{ bmc_password }}"
    register: rfsession

  - debug:
      var: rfsession
```

### Expected Results

on an iDRAC 8 node:

```
PLAY [Manager test] *************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

TASK [Create Redfish session] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
changed: [localhost] => {"changed": true, "msg": "Action was successful", "return_values": {}, "session": {"token": "REDACTED", "uri": "/redfish/v1/Sessions/8084"}}

TASK [debug] ********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "rfsession": {
        "changed": true,
        "failed": false,
        "msg": "Action was successful",
        "return_values": {},
        "session": {
            "token": "REDACTED",
            "uri": "/redfish/v1/Sessions/8084"
        }
    }
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

### Actual Results

On an iDRAC9 node:

```
PLAY [Manager test] *************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************

TASK [Create Redfish session] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "HTTP Error 415 on POST request to 'https://REDACTED/redfish/v1/SessionService/Sessions', extended message: 'A general error has occurred. See Resolution for information on how to resolve the error.'"}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/redfish_command.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/redfish_command.py)
* [`plugins/modules/redfish_config.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/redfish_config.py)
* [`plugins/modules/redfish_info.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/redfish_info.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @bhavya06 @mraineri @rajeevkallur @renxulei @tomasg2012 @xmadsen
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I suspect based on your debugging so far that the presence of the `Authorization` header (due to `force_basic_auth` being set to true) causes some services to reject the request. Providing an `Authorization` header while also providing credentials in the request body to make the session is unexpected. I can see cases where some services will ignore the header, and others that will err on the side of caution to raise this as a potential conflict; allowing both to pass through can give nondeterministic behavior (which credentials are consumed, the header's or the body's?). We don't have anything in the Redfish Specification as far as I can tell to recommend best practices, but specifically for Ansible, I think we should disable the `force_basic_auth` flag when performing the session creation request.

Resolving patch:
diff --git a/plugins/module_utils/redfish_utils.py b/plugins/module_utils/redfish_utils.py
--- a/plugins/module_utils/redfish_utils.py
+++ b/plugins/module_utils/redfish_utils.py
@@ -36,6 +36,8 @@ def __init__(self, creds, root_uri, timeout, module, resource_id=None,
         self.timeout = timeout
         self.module = module
         self.service_root = '/redfish/v1/'
+        self.session_service_uri = '/redfish/v1/SessionService'
+        self.sessions_uri = '/redfish/v1/SessionService/Sessions'
         self.resource_id = resource_id
         self.data_modification = data_modification
         self.strip_etag_quotes = strip_etag_quotes
@@ -66,6 +68,10 @@ def get_request(self, uri):
         req_headers = dict(GET_HEADERS)
         username, password, basic_auth = self._auth_params(req_headers)
         try:
+            # Service root is an unauthenticated resource; remove credentials
+            # in case the caller will be using sessions later.
+            if uri == (self.root_uri + self.service_root):
+                basic_auth = False
             resp = open_url(uri, method="GET", headers=req_headers,
                             url_username=username, url_password=password,
                             force_basic_auth=basic_auth, validate_certs=False,
@@ -92,6 +98,11 @@ def post_request(self, uri, pyld):
         req_headers = dict(POST_HEADERS)
         username, password, basic_auth = self._auth_params(req_headers)
         try:
+            # When performing a POST to the session collection, credentials are
+            # provided in the request body.  Do not provide the basic auth
+            # header since this can cause conflicts with some services
+            if self.sessions_uri is not None and uri == (self.root_uri + self.sessions_uri):
+                basic_auth = False
             resp = open_url(uri, data=json.dumps(pyld),
                             headers=req_headers, method="POST",
                             url_username=username, url_password=password,
@@ -232,23 +243,23 @@ def _find_accountservice_resource(self):
         return {'ret': True}
 
     def _find_sessionservice_resource(self):
+        # Get the service root
         response = self.get_request(self.root_uri + self.service_root)
         if response['ret'] is False:
             return response
         data = response['data']
-        if 'SessionService' not in data:
+
+        # Check for the session service and session collection.  Well-known
+        # defaults are provided in the constructor, but services that predate
+        # Redfish 1.6.0 might contain different values.
+        self.session_service_uri = data.get('SessionService', {}).get('@odata.id')
+        self.sessions_uri = data.get('Links', {}).get('Sessions', {}).get('@odata.id')
+
+        # If one isn't found, return an error
+        if self.session_service_uri is None:
             return {'ret': False, 'msg': "SessionService resource not found"}
-        else:
-            session_service = data["SessionService"]["@odata.id"]
-            self.session_service_uri = session_service
-            response = self.get_request(self.root_uri + session_service)
-            if response['ret'] is False:
-                return response
-            data = response['data']
-            sessions = data['Sessions']['@odata.id']
-            if sessions[-1:] == '/':
-                sessions = sessions[:-1]
-            self.sessions_uri = sessions
+        if self.sessions_uri is None:
+            return {'ret': False, 'msg': "SessionCollection resource not found"}
         return {'ret': True}
 
     def _get_resource_uri_by_id(self, uris, id_prop):


Test patch: