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

Run as UserID and Error handling changes #57

Merged
merged 31 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
12cd2b0
Error and Error Handling Changes
ElijahSwiftIBM Nov 24, 2023
f5754c2
Add base functionality to run as another user
ElijahSwiftIBM Nov 24, 2023
721e878
Typing changes
ElijahSwiftIBM Nov 24, 2023
a6e53c4
Update irrsmo00.py
ElijahSwiftIBM Nov 24, 2023
f4756c7
Surface IRRSMO00 Return and Reason Codes
ElijahSwiftIBM Nov 25, 2023
f261c44
Merge branch 'feature/handle-empty-return' into feature/run_as_userid
ElijahSwiftIBM Nov 25, 2023
85e7109
Fold Surrogat Error into Null Response
ElijahSwiftIBM Nov 26, 2023
a6e927d
Minor changes for doc
ElijahSwiftIBM Nov 26, 2023
723a381
Refactoring and Standardization Changes
ElijahSwiftIBM Nov 29, 2023
a1b9be1
Updates
ElijahSwiftIBM Nov 29, 2023
eda3184
Complete Transition to UserIdError
ElijahSwiftIBM Nov 29, 2023
8db616c
Changing error text and formatting
ElijahSwiftIBM Nov 30, 2023
3c594f5
Rearrange Errors
ElijahSwiftIBM Nov 30, 2023
8a227de
add running_user even when not specified
ElijahSwiftIBM Dec 1, 2023
000b71f
Name Changes
ElijahSwiftIBM Dec 4, 2023
04c028d
Naming and Docstring Updates
ElijahSwiftIBM Dec 4, 2023
46be4b8
Naming and convention fixes
ElijahSwiftIBM Dec 4, 2023
d05d4c4
Minor changes
ElijahSwiftIBM Dec 6, 2023
2baa080
Minor changes
ElijahSwiftIBM Dec 6, 2023
3c4bee2
Add unit test for Add operations
ElijahSwiftIBM Dec 12, 2023
a958401
Docstring changes
ElijahSwiftIBM Dec 18, 2023
dbdf9c1
Add Testcase for SetupPrecheck error
ElijahSwiftIBM Dec 19, 2023
2d7c512
Update test_setup_precheck.py
ElijahSwiftIBM Dec 19, 2023
588449f
Update security_admin.py
ElijahSwiftIBM Dec 19, 2023
07c92cc
Change DownstreamFatalError Test Constants
ElijahSwiftIBM Dec 19, 2023
f9db7fc
Update irrsmo00.py
ElijahSwiftIBM Dec 19, 2023
37b2214
Update irrsmo00.c
ElijahSwiftIBM Dec 20, 2023
0baee16
Function and Unit Test Changes
ElijahSwiftIBM Dec 20, 2023
19c5df0
Change docstrings and version number
ElijahSwiftIBM Dec 21, 2023
af81fc7
Drive userid on get_running_userid to lowercase
ElijahSwiftIBM Dec 26, 2023
f4171c2
Update security_admin.py
ElijahSwiftIBM Jan 3, 2024
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
3 changes: 3 additions & 0 deletions pyracf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
from .access.access_admin import AccessAdmin
from .common.add_operation_error import AddOperationError
from .common.alter_operation_error import AlterOperationError
from .common.downstream_fatal_error import DownstreamFatalError
from .common.security_request_error import SecurityRequestError
from .common.segment_error import SegmentError
from .common.segment_trait_error import SegmentTraitError
from .common.userid_error import UserIdError
from .connection.connection_admin import ConnectionAdmin
from .data_set.data_set_admin import DataSetAdmin
from .group.group_admin import GroupAdmin
from .resource.resource_admin import ResourceAdmin
from .scripts.setup_precheck import setup_precheck
from .setropts.setropts_admin import SetroptsAdmin
from .user.user_admin import UserAdmin
2 changes: 2 additions & 0 deletions pyracf/access/access_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(
update_existing_segment_traits: Union[dict, None] = None,
replace_existing_segment_traits: Union[dict, None] = None,
additional_secret_traits: Union[List[str], None] = None,
run_as_userid: Union[str, None] = None,
) -> None:
self._valid_segment_traits = {
"base": {
Expand Down Expand Up @@ -48,6 +49,7 @@ def __init__(
update_existing_segment_traits=update_existing_segment_traits,
replace_existing_segment_traits=replace_existing_segment_traits,
additional_secret_traits=additional_secret_traits,
run_as_userid=run_as_userid,
)

# ============================================================================
Expand Down
69 changes: 69 additions & 0 deletions pyracf/common/downstream_fatal_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Exception to use when IRRSMO00 is unable to process a request."""
from typing import Union


class DownstreamFatalError(Exception):
"""
Raised IRRSMO00 returns with a SAF Return Code of 8,
lcarcaramo marked this conversation as resolved.
Show resolved Hide resolved
indicating that the request could not be processed.
"""
lcarcaramo marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
saf_return_code: int,
racf_return_code: int,
racf_reason_code: int,
request_xml: bytes,
run_as_userid: Union[str, None] = None,
result_dictionary: dict = None,
) -> None:
self.message = "Security request made to IRRSMO00 failed."
self.saf_return_code = saf_return_code
self.racf_return_code = racf_return_code
self.racf_reason_code = racf_reason_code
self.request_xml = request_xml.decode("utf-8")
self.message += (
f"\n\nSAF Return Code: {self.saf_return_code}\nRACF Return Code:"
+ f" {self.racf_return_code}\nRACF Reason Code: {self.racf_reason_code}"
)
if result_dictionary is not None:
self.message += (
"\n\nSee results dictionary "
+ f"'{self.__class__.__name__}.result' for more details.\n"
+ "\n\nYou can also check the specified return and reason codes against "
+ "the documented IRRSMO00 return and reason codes for more information "
+ "about this error.\n"
+ "https://www.ibm.com/docs/en/zos/3.1.0?topic=operations-return-reason-codes"
)
self.result = result_dictionary
elif (
(self.saf_return_code == 8)
and (self.racf_return_code == 200)
and (self.racf_reason_code == 16)
):
self.message += (
"\n\nCheck to see if the proper RACF permissions are in place.\n"
+ "For 'set' or 'alter' functions, you must have at least 'READ' "
+ "access to 'IRR.IRRSMO00.PRECHECK' in the 'XFACILIT' class."
)
elif (
(self.saf_return_code == 8)
and (self.racf_return_code == 200)
and (self.racf_reason_code == 8)
):
self.message += (
"\n\nCheck to see if the proper RACF permissions are in place.\n"
+ "For the 'run_as_userid' feature, you must have at least 'UPDATE' "
+ f"access to '{run_as_userid}.IRRSMO00' in the 'SURROGAT' class."
)
else:
self.message += (
"\n\nPlease check the specified return and reason codes against "
+ "the documented IRRSMO00 return and reason codes for more information "
+ "about this error.\n"
+ "https://www.ibm.com/docs/en/zos/3.1.0?topic=operations-return-reason-codes"
)
self.message = f"({self.__class__.__name__}) {self.message}"

def __str__(self) -> str:
return self.message
15 changes: 10 additions & 5 deletions pyracf/common/irrsmo00.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE (100000)

Expand Down Expand Up @@ -33,25 +34,29 @@ void null_byte_fix(char* str, unsigned int str_len) {
static PyObject* call_irrsmo00(PyObject* self, PyObject* args, PyObject *kwargs) {
const unsigned int xml_len;
const unsigned int input_opts;
const uint8_t input_userid_len;
const char *input_xml;
const char *input_userid;

static char *kwlist[] = {"xml_str", "xml_len", "opts", NULL};
static char *kwlist[] = {"xml_str", "xml_len", "opts", "userid", "userid_len", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y|II", kwlist, &input_xml, &xml_len, &input_opts)) {
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y|IIyb", kwlist, &input_xml, &xml_len, &input_opts, &input_userid, &input_userid_len)) {
return NULL;
}


char work_area[1024];
char req_handle[64] = { 0 };
VarStr_T userid = { 0, {0}};
VarStr_T userid = { input_userid_len, {0}};
unsigned int alet = 0;
unsigned int acee = 0;
unsigned char rsp[BUFFER_SIZE+1];
memset(rsp, 0, BUFFER_SIZE);
unsigned int saf_rc=0, racf_rc=0, racf_rsn=0;
unsigned int num_parms=17, fn=1, opts = input_opts, rsp_len = sizeof(rsp)-1;

strncpy(userid.str, input_userid, userid.len);

IRRSMO64(
work_area,
alet,
Expand All @@ -73,11 +78,11 @@ static PyObject* call_irrsmo00(PyObject* self, PyObject* args, PyObject *kwargs)
);

null_byte_fix(rsp,rsp_len);
return Py_BuildValue("y", rsp);
return Py_BuildValue("yBBB", rsp, saf_rc, racf_rc, racf_rsn);
}

static char call_irrsmo00_docs[] =
"call_irrsmo00(input_xml: bytes, xml_len: uint, opts: uint): Returns an XML response from the IRRSMO00 RACF Callable Service.\n";
"call_irrsmo00(input_xml: bytes, xml_len: uint, opts: uint): Returns an XML response string and return and reason codes from the IRRSMO00 RACF Callable Service.\n";

static PyMethodDef cpyracf_methods[] = {
{"call_irrsmo00", (PyCFunction)call_irrsmo00,
Expand Down
26 changes: 22 additions & 4 deletions pyracf/common/irrsmo00.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Interface to irrsmo00.dll."""
import platform
from typing import Union

try:
from cpyracf import call_irrsmo00
Expand All @@ -19,9 +20,26 @@ def __init__(self) -> None:
# Initialize size of output buffer
self.buffer_size = 100000

def call_racf(self, request_xml: bytes, precheck: bool = False) -> str:
def call_racf(
lcarcaramo marked this conversation as resolved.
Show resolved Hide resolved
self,
request_xml: bytes,
precheck: bool = False,
run_as_userid: Union[str, None] = None,
) -> str:
"""Make request to call_irrsmo00 in the cpyracf Python extension."""
options = 15 if precheck else 13
return call_irrsmo00(
xml_str=request_xml, xml_len=len(request_xml), opts=options
).decode("cp1047")
userid = b""
userid_length = 0
if run_as_userid:
userid = run_as_userid.encode("cp1047")
userid_length = len(run_as_userid)
response = call_irrsmo00(
xml_str=request_xml,
xml_len=len(request_xml),
opts=options,
userid=userid,
userid_len=userid_length,
)
if response[0] == "":
return response[1:3]
return response[0].decode("cp1047")
14 changes: 9 additions & 5 deletions pyracf/common/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import os
import re
from typing import Union
from typing import List, Union


class Logger:
Expand Down Expand Up @@ -161,7 +161,7 @@ def redact_request_xml(

def redact_result_xml(
self,
xml_string: str,
security_response: Union[str, List[int]],
secret_traits: dict,
) -> str:
"""
Expand All @@ -170,13 +170,17 @@ def redact_result_xml(
'TRAIT (value)'
This function also accounts for varied amounts of whitespace in the pattern.
"""
if isinstance(security_response, list):
return security_response
for xml_key in secret_traits.values():
racf_key = xml_key.split(":")[1] if ":" in xml_key else xml_key
match = re.search(rf"{racf_key.upper()} +\(", xml_string)
match = re.search(rf"{racf_key.upper()} +\(", security_response)
if not match:
continue
xml_string = self.__redact_string(xml_string, match.end(), ")")
return xml_string
security_response = self.__redact_string(
security_response, match.end(), ")"
)
return security_response

def __colorize_json(self, json_text: str) -> str:
updated_json_text = ""
Expand Down
78 changes: 62 additions & 16 deletions pyracf/common/security_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from datetime import datetime
from typing import Any, List, Tuple, Union

from .downstream_fatal_error import DownstreamFatalError
from .irrsmo00 import IRRSMO00
from .logger import Logger
from .security_request import SecurityRequest
from .security_request_error import SecurityRequestError
from .security_result import SecurityResult
from .segment_error import SegmentError
from .segment_trait_error import SegmentTraitError
from .userid_error import UserIdError


class SecurityAdmin:
Expand All @@ -20,6 +22,7 @@ class SecurityAdmin:
_valid_segment_traits = {}
_extracted_key_value_pair_segment_traits_map = {}
_case_sensitive_extracted_values = []
__running_userid = None
__logger = Logger()

def __init__(
Expand All @@ -30,6 +33,7 @@ def __init__(
update_existing_segment_traits: Union[dict, None] = None,
replace_existing_segment_traits: Union[dict, None] = None,
additional_secret_traits: Union[List[str], None] = None,
run_as_userid: Union[str, None] = None,
) -> None:
self._common_base_traits_data_set_generic = {
"base:aclcnt": "racf:aclcnt",
Expand Down Expand Up @@ -79,11 +83,26 @@ def __init__(
self.__replace_valid_segment_traits(replace_existing_segment_traits)
if additional_secret_traits is not None:
self.__add_additional_secret_traits(additional_secret_traits)
self.set_running_userid(run_as_userid)

# ============================================================================
# Run as Other User ID
# ============================================================================
def set_running_userid(self, userid: Union[str, None]) -> None:
if userid is None:
self.__running_userid = None
return
if not isinstance(userid, str) or len(userid) > 8 or userid == "":
raise UserIdError(userid)
self.__running_userid = userid.upper()
lcarcaramo marked this conversation as resolved.
Show resolved Hide resolved

def get_running_userid(self) -> None:
return self.__running_userid

# ============================================================================
# Customize Segment Traits
# ============================================================================
def __update_valid_segment_traits(self, update_valid_segment_traits: dict):
def __update_valid_segment_traits(self, update_valid_segment_traits: dict) -> None:
"""Update fields to valid segment traits dictionary."""
for segment in update_valid_segment_traits:
if segment in self._valid_segment_traits:
Expand All @@ -95,14 +114,14 @@ def __update_valid_segment_traits(self, update_valid_segment_traits: dict):
segment
]

def __replace_valid_segment_traits(self, new_valid_segment_traits: dict):
def __replace_valid_segment_traits(self, new_valid_segment_traits: dict) -> None:
"""Replace field data in valid segment traits dictionary"""
self._valid_segment_traits = new_valid_segment_traits

# ============================================================================
# Secrets Redaction
# ============================================================================
def __add_additional_secret_traits(self, additional_secret_traits: list):
def __add_additional_secret_traits(self, additional_secret_traits: list) -> None:
"""Add additional fields to be redacted in logger output."""
for secret in additional_secret_traits:
if secret in self.__secret_traits:
Expand Down Expand Up @@ -157,36 +176,63 @@ def _make_request(
security_request.dump_request_xml(encoding="utf-8"),
secret_traits=self.__secret_traits,
)
request_xml = self.__logger.redact_request_xml(
security_request.dump_request_xml(encoding="utf-8"),
secret_traits=self.__secret_traits,
)
if self._generate_requests_only:
request_xml = self.__logger.redact_request_xml(
security_request.dump_request_xml(encoding="utf-8"),
secret_traits=self.__secret_traits,
)
self.__clear_state(security_request)
return request_xml
result_xml = self.__logger.redact_result_xml(
raw_result = self.__logger.redact_result_xml(
self.__irrsmo00.call_racf(
security_request.dump_request_xml(), irrsmo00_precheck
security_request.dump_request_xml(),
irrsmo00_precheck,
self.__running_userid,
),
self.__secret_traits,
)
self.__clear_state(security_request)
if isinstance(raw_result, list):
# When IRRSMO00 encounters some errors, it returns no XML response string.
# When this happens, the C code instead surfaces the return and reason
# codes which causes a DownstreamFatalError to be raised.
raise DownstreamFatalError(
saf_return_code=raw_result[0],
racf_return_code=raw_result[1],
racf_reason_code=raw_result[2],
request_xml=request_xml,
run_as_userid=self.get_running_userid(),
)
if self.__debug:
# No need to redact anything here since the raw result XML
# already has secrets redacted when it is built.
self.__logger.log_xml("Result XML", result_xml)
results = SecurityResult(result_xml)
self.__logger.log_xml("Result XML", raw_result)
result = SecurityResult(raw_result, self.get_running_userid())
if self.__debug:
# No need to redact anything here since the result dictionary
# already has secrets redacted when it is built.
self.__logger.log_dictionary(
"Result Dictionary", results.get_result_dictionary()
"Result Dictionary", result.get_result_dictionary()
)
result_dictionary = result.get_result_dictionary()
if result_dictionary["securityResult"]["returnCode"] >= 8:
# All return codes greater than 4 are indicative of an issue with
# IRRSMO00 that would stop a command image from being generated.
# The user should interogate the result dictionary attached to the
# SecurityRequestError as well as the return and reason codes to
# resolve the problem.
raise DownstreamFatalError(
saf_return_code=8,
racf_return_code=result_dictionary["securityResult"]["returnCode"],
racf_reason_code=result_dictionary["securityResult"]["reasonCode"],
request_xml=request_xml,
run_as_userid=self.get_running_userid(),
result_dictionary=result_dictionary,
)
result_dictionary = results.get_result_dictionary()
if result_dictionary["securityResult"]["returnCode"] != 0:
# All non-zero return codes should cause a SecurityRequestError to be raised.
# Even if a return code of 4 is not indicative of a problem, it it is
# up to the user to interogate the result dictionary attached to the
# All remaining non-zero return codes should cause a SecurityRequestError
# to be raised. Even if a return code of 4 is not indicative of a problem,
# it is up to the user to interogate the result dictionary attached to the
# SecurityRequestError and decided whether or not the return code 4 is
# indicative of a problem.
raise SecurityRequestError(result_dictionary)
Expand Down
Loading