Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Redfish PasswordChangeRequired #103

Closed
joseph-reynolds opened this issue Sep 12, 2019 · 5 comments
Closed

Support Redfish PasswordChangeRequired #103

joseph-reynolds opened this issue Sep 12, 2019 · 5 comments

Comments

@joseph-reynolds
Copy link
Contributor

This issue is to track efforts to implement the Redfish PasswordChangeRequired handling in BMCWeb.

The PasswordChangeRequired handling is new in the Redfish Spec DSP0266 with version 1.7.0 dated 2019-05-16, in section 13.2.6.1 ("Password change required handling")

Background:

  • BMCWeb maps OpenBMC privilege roles to Redfish Roles.
  • Redfish maps Redfish roles to Redfish privileges and is defined in the Redfish spec, section 13.2.9 ("Privilege model/Authorization").
  • The Redfish ConfigureSelf privilege refers specifically to the actions needed to change your own password using Redfish APIs.

So the main idea is to enhance BMCWeb to recognize when the user's password is correct but expired, and create a Redfish session which has only the Redfish ConfigureSelf privilege.

@joseph-reynolds
Copy link
Contributor Author

The initial commit for this is ready to review. This commit is intended to be used by the web application to know when when an expired password was presented during login, so the web app can present the password change dialog. This commit does not have all the changes needed to actually change the expired password, and I am working on a follow commit. See the gerrit review commit message for more details: https://gerrit.openbmc-project.xyz/c/openbmc/bmcweb/+/25146

This was unit tested on a QEMU Romulus system with the following script:

Bash helper functions to help unit test

: These store the Redfish session ID and session X-Auth-Tokens
declare -A sessid
declare -A authtok

function create-redfish-session()
{
  : Purpose: This creates a Redfish login session and saves the session X-Auth-Token and session ID
  : in shell variables so you can use the session for subsequent access and terminate the session properly.
  : Parameters: session_name, userid, and password are required.
  : Results:
  :   If the session is created, the session id and auth-token are stored in shell variables.
  :   If the password must be changed, PasswordChangeRequiredURI is set.
  :   test -n $"${sessid[${session_name}]}" is true if the session was created.
  session_name=$1   # Your name for the session
  local userid=$2
  local password=$3
  curl --insecure -X POST -D headers.txt https://${bmc}:2443/redfish/v1/SessionService/Sessions -d '{"UserName":"'${userid}'", "Password":"'${password}'"}' | tee results.txt
  authtok[${session_name}]=$(grep "^X-Auth-Token: " headers.txt | cut -d' ' -f2 | tr -d '\r')
  test -n "${authtok[${session_name}]}" && echo "Got X-Auth-Token okay" || { echo "Failed to get X-Auth-Token" && false; }
  sessid[${session_name}]=$(grep -e '"Id":' results.txt | cut -d\" -f4)
  echo "sessid[${session_name}]='${sessid[${session_name}]}'"

  : Detect if password change is needed and set PasswordChangeRequiredURI as needed.
  read -r -d '' python_code_to_detect_PasswordChangeRequired << EOM
import sys,json
j = json.load(sys.stdin)
if "@Message.ExtendedInfo" in j:
  for err_info in j["@Message.ExtendedInfo"]:
      if "PasswordChangeRequired" in err_info["MessageId"]:
        sys.stdout.write(err_info["MessageArgs"][0])
EOM
  PasswordChangeRequiredURI=$(python -c "$python_code_to_detect_PasswordChangeRequired" <results.txt)
  if test -n "${PasswordChangeRequiredURI}"; then
    echo "The password is expired and must be changed, for example:"
    echo '  PATCH '${PasswordChangeRequiredURI}' with {"Password": "NEWPASSWORD"}'
  fi
}

function use-redfish-session()
{
  : Purpose: Use a session created by create-redfish-session
  session_name=$1; shift
  curl --insecure -H "X-Auth-Token: ${authtok[${session_name}]}" "$@"
}

function terminate-redfish-session()
{
  : Purpose: Terminate a session created by create-redfish-session
  session_name=$1; shift
  use-redfish-session ${session_name} -X DELETE  https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[${session_name}]}
  unset sessid[${session_name}]
  unset authtok[${session_name}]
}

function use-ssh()
{
  : Purpose: ssh to the BMC
  ssh -p 2222 root@${bmc} "$@"
}

Unit test script

: Login as root
create-redfish-session root_session root 0penBmc
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[root_session]}
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/root 

: Add a test user account
TESTUSER=testuser
TESTPASSWORD1=TestP4ssw0rd
TESTPASSWORD2=N3wTestPswd
TESTPASSWORD3=Th1rdB4dPswd
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/$TESTUSER
use-redfish-session root_session -X POST  https://${bmc}:2443/redfish/v1/AccountService/Accounts/ -d '{"UserName": "'$TESTUSER'", "Password": "'$TESTPASSWORD1'", "RoleId": "Administrator"}'
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/$TESTUSER

: Login as the test user
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD1}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[TEST_session]}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
terminate-redfish-session TEST_session

: Use ssh to administratively expire the test user's password
: Can this be done from Redfish?
use-ssh  passwd --expire ${TESTUSER}
: You should see a message like: passwd: password expiry information changed

: Try /login to testuser --- this tells you when password change required, but does not create a session
curl -k -D login.headers -H "Content-Type: application/json" -X POST https://${bmc}:2443/login -d '{"data": ["'$TESTUSER'", "'$TESTPASSWORD1'"]}'
: You should see the response: Password change required
: You should see "401 Unauthorized" in the login.headers file

: Try basic auth -- this handles password change required same as unauthorized
curl -k -D basic-auth.headers  -X GET https://${TESTUSER}:${TESTPASSWORD1}@${bmc}:2443/redfish/v1/SessionService
: You should see the response: Unauthorized
: You should see "401 Unauthorized" in the login.headers file

: Login as the test user with expired password
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD1}
: This should create a session with the following additional field:
  "@Message.ExtendedInfo": [
    {
      "@odata.type": "/redfish/v1/$metadata#Message.v1_5_0.Message",
      "Message": "The password provided for this account must be changed before access is granted.  PATCH the 'Password' property for this account located at the target URI '/redfish/v1/AccountService/Accounts/testuser' to complete this process.",
      "MessageArgs": [
        "/redfish/v1/AccountService/Accounts/testuser"
      ],
      "MessageId": "Base.1.5.0.PasswordChangeRequired",
      "Resolution": "Change the password for this account using a PATCH to the 'Password' property at the URI provided.",
      "Severity": "Critical"
    }
  ],
: validate bmcweb log messages
use-ssh  journalctl --unit=bmcweb
:  Validate there is an entry like: bmcweb: pam_unix(webserver:account): expired password for user joseph (root enforced)
: Validate TEST_session can GET session and account data
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[TEST_session]}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
: TODO: Validate TEST_session cannot access other resources:
:   - Various resources (GET, PATCH, POST)
:   - Especially check we cannot access other accounts or other sessions
:     GET https://${bmc}:2443/redfish/v1/AccountService/Accounts -- should be forbidden
: PATCH a new Password into the account
use-redfish-session TEST_session -X PATCH https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER} -d '{"Password": "'$TESTPASSWORD2'"}'
: Use ssh to validate the password actually changed
use-ssh  passwd --status ${TESTUSER}
:  TO DO: Validate journalctl message = pam_unix(passwd:chauthtok): password changed for testuser
: Validate TEST_session can terminate
terminate-redfish-session TEST_session

: Validate access using new password
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD2}
: TODO: Validate the session is not PasswordChangeRequired
: TODO: Validate we have the normal access
terminate-redfish-session TEST_session

: Back to the root_session: cleanup (delete $TESTUSER)
use-redfish-session root_session -X DELETE  https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
terminate-redfish-session root_session

The server failed to allow the user to change their own expired password. The next commit will allow this.

@joseph-reynolds
Copy link
Contributor Author

joseph-reynolds commented Sep 26, 2019

Working on the updated design proposal to allow users to use /login with an expired password. See the:

Bash helper functions to help unit test

: These store the Redfish session ID and session X-Auth-Tokens
declare -A sessid
declare -A authtok

function create-redfish-session()
{
  : Purpose: This creates a Redfish login session and saves the session X-Auth-Token and session ID
  : in shell variables so you can use the session for subsequent access and terminate the session properly.
  : Parameters: session_name, userid, and password are required.
  : Results:
  :   If the session is created, the session id and auth-token are stored in shell variables.
  :   If the password must be changed, PasswordChangeRequiredURI is set.
  :   test -n $"${sessid[${session_name}]}" is true if the session was created.
  session_name=$1   # Your name for the session
  local userid=$2
  local password=$3
  curl --insecure -X POST -D headers.txt https://${bmc}:2443/redfish/v1/SessionService/Sessions -d '{"UserName":"'${userid}'", "Password":"'${password}'"}' | tee results.txt
  authtok[${session_name}]=$(grep "^X-Auth-Token: " headers.txt | cut -d' ' -f2 | tr -d '\r')
  test -n "${authtok[${session_name}]}" && echo "Got X-Auth-Token okay" || { echo "Failed to get X-Auth-Token" && false; }
  sessid[${session_name}]=$(grep -e '"Id":' results.txt | cut -d\" -f4)
  echo "sessid[${session_name}]='${sessid[${session_name}]}'"

  : Detect if password change is needed and set PasswordChangeRequiredURI as needed.
  read -r -d '' python_code_to_detect_PasswordChangeRequired << EOM
import sys,json
j = json.load(sys.stdin)
if "@Message.ExtendedInfo" in j:
  for err_info in j["@Message.ExtendedInfo"]:
      if "PasswordChangeRequired" in err_info["MessageId"]:
        sys.stdout.write(err_info["MessageArgs"][0])
EOM
  PasswordChangeRequiredURI=$(python -c "$python_code_to_detect_PasswordChangeRequired" <results.txt)
  if test -n "${PasswordChangeRequiredURI}"; then
    echo "The password is expired and must be changed, for example:"
    echo '  PATCH '${PasswordChangeRequiredURI}' with {"Password": "NEWPASSWORD"}'
  fi
}

function create-unauthenticated-session-credentials()
{
  : Purpose: set up a session_name so for unauthenticated access to the BMC
  : No username or password are needed
  session_name=$1   # Your name for the session
  authtok[${session_name}]="BAD-not-authenticated"
  sessid[${session_name}]="BAD-not-authenticated"
}

function use-redfish-session()
{
  : Purpose: Use a session created by create-redfish-session
  session_name=$1; shift
  curl --insecure -H "X-Auth-Token: ${authtok[${session_name}]}" "$@"
}

function terminate-redfish-session()
{
  : Purpose: Terminate a session created by create-redfish-session
  session_name=$1; shift
  use-redfish-session ${session_name} -X DELETE  https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[${session_name}]}
  unset sessid[${session_name}]
  unset authtok[${session_name}]
}

function terminate-all-other-redfish-sessions()
{
  : Purpose: Terminates ALL sessions except the session passed in to this function.
  : Required admin access
  session_name=$1; shift
  mysessid=${sessid[${session_name}]}
  mysess_uri="/redfish/v1/SessionService/Sessions/${mysessid}"
  read -r -d '' python_code_to_slurp_sessions << EOM
import sys,json
j = json.load(sys.stdin)
for sessinfo in j["Members"]:
  sys.stdout.write(sessinfo["@odata.id"] + '\n')
EOM
  session_uris=$(use-redfish-session ${session_name} -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions |
    python -c "$python_code_to_slurp_sessions")
  for sess_uri in $session_uris; do
    if test ${sess_uri} != ${mysess_uri}; then
      echo "Deleting $sess_uri"
      use-redfish-session ${session_name} -X DELETE  https://${bmc}:2443${sess_uri}
    fi
  done
}

function use-ssh()
{
  : Purpose: ssh to the BMC
  ssh -p 2222 root@${bmc} "$@"
}

Unit test script

: Login as root
create-redfish-session root_session root 0penBmc
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[root_session]}
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/root 

: Second login as root
create-redfish-session root_session2 root 0penBmc
use-redfish-session root_session2 -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[root_session]}
use-redfish-session root_session2 -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/root 

: Add testuser account
TESTUSER=testuser
TESTPASSWORD1=TestP4ssw0rd
TESTPASSWORD2=N3wTestPswd
TESTPASSWORD3=Th1rdB4dPswd
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/$TESTUSER
use-redfish-session root_session -X DELETE  https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
use-redfish-session root_session -X POST  https://${bmc}:2443/redfish/v1/AccountService/Accounts/ -d '{"UserName": "'$TESTUSER'", "Password": "'$TESTPASSWORD1'", "RoleId": "User"}'
use-redfish-session root_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/$TESTUSER

: Try with RoleId - Administrator, Operator, User

: Login testuser via Redfish
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD1}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[TEST_session]}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
terminate-redfish-session TEST_session

: Expire testuser password via ssh access as root
: Can this be done from Redfish?
use-ssh  passwd --expire ${TESTUSER}
: Enter the  TESTUSER password from above (NOT 0penBmc)
: You should see a message like: passwd: password expiry information changed

TODO -- this will change
TODO: Also try the alternate syntax...this worked:
         -X POST https://${bmc}:2443/login -d '{"username":"testuser", "password":"TestP4ssw0rd"}'
TODO: Also try HTTP headers: curl -H "username: ${TESTUSER}" -H "password: ${TESTPASSWORD1}"

: TRY Login testuser via /login
curl -k -D login.headers -H "Content-Type: application/json" -X POST https://${bmc}:2443/login -d '{"data": ["'$TESTUSER'", "'$TESTPASSWORD1'"]}'
: You should see the response: 200 OK with the errorExtendedInfo message
login_xauthtok=$(grep SESSION= login.headers | cut -d= -f2 | cut -d';' -f1) && echo ${login_xauthtok}
: Use that session for regular access
curl -k -D login.headers -H "Content-Type: application/json" -H "X-Auth-Token: ${login_xauthtok}" -X GET https://${bmc}:2443/list
grep ^HTTP login.headers
:    Validate http status "403 Forbidden"
: Try to change the password via Redfish API:
curl -k -D login.headers -H "Content-Type: application/json" -H "X-Auth-Token: ${login_xauthtok}" -X PATCH https://${bmc}:2443/redfish/v1/AccountService/Accounts/testuser -d '{"Password":"'${TESTPASSWORD2}'"}'

: Try: ssh to the BMC using the user with the expired password
: this tells you the password is expired, but does not implement the password change dialog
ssh -p 2222 ${TESTUSER}@${bmc}
:   The result I get from dropbear SSH server is:
:    You are required to change your password immediately (administrator enforced)
:    Permission denied, please try again.
:    testuser@BMC's password: 

: Try basic auth -- this handles password change required same as unauthorized
curl -k -D basic-auth.headers  -X GET https://${TESTUSER}:${TESTPASSWORD1}@${bmc}:2443/redfish/v1/SessionService
: You should see the response: Unauthorized
: You should see "401 Unauthorized" in the basic-auth.headers file

: Login as the test user with expired password
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD1}
: This should create a session with the following additional field:
  "@Message.ExtendedInfo": [
    {
      "@odata.type": "/redfish/v1/$metadata#Message.v1_5_0.Message",
      "Message": "The password provided for this account must be changed before access is granted.  PATCH the 'Password' property for this account located at the target URI '/redfish/v1/AccountService/Accounts/testuser' to complete this process.",
      "MessageArgs": [
        "/redfish/v1/AccountService/Accounts/testuser"
      ],
      "MessageId": "Base.1.5.0.PasswordChangeRequired",
      "Resolution": "Change the password for this account using a PATCH to the 'Password' property at the URI provided.",
      "Severity": "Critical"
    }
  ],
: validate bmcweb log messages
use-ssh  journalctl --unit=bmcweb
:  Validate there is an entry like: bmcweb: pam_unix(webserver:account): expired password for user testuser (root enforced)
: Validate TEST_session can GET session and account data
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/SessionService/Sessions/${sessid[TEST_session]}
use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
: TODO: Validate TEST_session cannot access other resources:
:   - Various resources (GET, PATCH, POST)
:   - Especially check we cannot access other accounts or other sessions
:   use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts/root
:   use-redfish-session TEST_session -X GET https://${bmc}:2443/redfish/v1/AccountService/Accounts
:     validate is forbidden
:     validate we get appropriate @Message.ExtendedInfo: AccessDenied and PasswordChangeRequired
: Validate with the DBus REST APIs:
use-redfish-session TEST_session -X GET https://${bmc}:2443/org/BAD
use-redfish-session TEST_session -X GET https://${bmc}:2443/xyz/BAD
use-redfish-session TEST_session -X GET https://${bmc}:2443/bus/system/BAD
:    validate responses

: PATCH a new Password into the account
use-redfish-session TEST_session -X PATCH https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER} -d '{"Password": "'$TESTPASSWORD2'"}'
: Use ssh to validate the password actually changed
use-ssh  passwd --status ${TESTUSER}
:   Validate the result shows the letter P followed by the current date
:  TO DO: Use the `journalctl --unit=bmcweb` to validate log message: pam_unix(passwd:chauthtok): password changed for testuser
: Validate TEST_session can terminate
terminate-redfish-session TEST_session

: Validate access using new password
create-redfish-session TEST_session ${TESTUSER} ${TESTPASSWORD2}
: TODO: Validate the session is not PasswordChangeRequired
: TODO: Validate we have the normal access
terminate-redfish-session TEST_session

: Back to the root_session: cleanup (delete $TESTUSER)
use-redfish-session root_session -X DELETE  https://${bmc}:2443/redfish/v1/AccountService/Accounts/${TESTUSER}
terminate-redfish-session root_session

==> Interesting test result: When an Admin user DELETEs a session which is subject to PasswordChangeRequired handling, the HTTP response message contains the resource it successfully deleted, namely the Session including the PasswordChangeRequired extended message. In this case, it is ambiguous if the PasswordChangeRequired message refers to the session which was deleted or to the DELETE request itself.

@joseph-reynolds
Copy link
Contributor Author

Per the code review comments, removed the special case code to handle PATCH the Password property in /redfish/v1/AccountService/Accounts/testuser. This method returns HTTP status 200 (OK) for success, and currently returns 500 (Internal server error) for failure, with an empty HTTP response body (Content-Length: 0). Callers are advised to test for (HTTP_status == 200).

@joseph-reynolds
Copy link
Contributor Author

The Password change REST API PATCH /redfish/v1/AccountService/Accounts/${TESTUSER} with JSON request data {"Password": "NEWPASSWORD"}) does not currently give any indication about what went wrong with the password change request. Some information is available from the underlying PAM chauthtok interface, but that information is not being returned to the REST API caller. Reference: In https://github.com/openbmc/bmcweb/blob/master/include./pam_authenticate.hpp function pamUpdatePassword() calls pam_chauthtok().

@joseph-reynolds
Copy link
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant