diff --git a/fprc/README.md b/fprc/README.md new file mode 100644 index 000000000..4de364a9f --- /dev/null +++ b/fprc/README.md @@ -0,0 +1,26 @@ +# fprc +We will need an active iRODS server running on localhost at port 1247. +Default install is fine. + +Change directory to the repository root (or one level above fprc/). + +To run demos: +```bash +itouch testobj.dat +imeta set -d testobj.dat attr value units +for script python3/demo*py: +do + command ${script} +done +``` + +To run tests installation +is needed: + +```bash +sudo apt install python3-pytest python3-venv +python -m venv py3-fprc +source py3-fprc/bin/activate +pip install fprc/ +python -m pytest fprc/ +``` diff --git a/fprc/demo_connect.py b/fprc/demo_connect.py new file mode 100755 index 000000000..319301639 --- /dev/null +++ b/fprc/demo_connect.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +from irods.client.connection import Connection +from irods.client.account import iRODSAccount + +account = iRODSAccount( + 'localhost', + 1247, + 'rods', + 'tempZone', + password='rods') +conn = Connection(account, authenticate=False) +print(conn.server_version) diff --git a/fprc/demo_dataobj.py b/fprc/demo_dataobj.py new file mode 100755 index 000000000..96b8fe4dc --- /dev/null +++ b/fprc/demo_dataobj.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +from irods.client.connection import Connection +from irods.client.account import iRODSAccount +from irods.client.query import Query +from irods.client.query.models import DataObject,Collection +from irods.client.query.column import Like + +account = iRODSAccount( + 'localhost', + 1247, + 'rods', + 'tempZone', + password='rods' +) + +conn = Connection( account ) + +from irods.client.data import ( + data_open, + data_write, + data_read, + data_close, + data_seek, + O_RDWR, + O_CREAT, +) + +desc = data_open(conn, '/tempZone/home/rods/abc.dat', O_RDWR|O_CREAT) +data_write(conn, desc, b'hello world') +data_seek(conn, desc, 1) +print( "Read from data object:", + data_read(conn, desc, 100).decode() +) +data_close(conn, desc) diff --git a/fprc/demo_query1.py b/fprc/demo_query1.py new file mode 100755 index 000000000..74625c57d --- /dev/null +++ b/fprc/demo_query1.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +from irods.client.connection import Connection +from irods.client.account import iRODSAccount +from irods.client.query import Query +from irods.client.query.models import DataObject,Collection +from irods.client.query.column import Like + +account = iRODSAccount( + 'localhost', + 1247, + 'rods', + 'tempZone', + password='rods' +) + +conn = Connection( account ) + +for row in Query(conn, [DataObject, Collection.name]).filter(Like(DataObject.name, 'a%')): + coll = row[Collection.name] + id = row[DataObject.id] + data = row[DataObject.name] + print( f'{id}: {coll}/{data}') diff --git a/fprc/demo_query2.py b/fprc/demo_query2.py new file mode 100755 index 000000000..1a60f1618 --- /dev/null +++ b/fprc/demo_query2.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +from irods.client.connection import Connection +from irods.client.account import iRODSAccount +from irods.client.query import Query +from irods.client.query.models import DataObject,Collection +from irods.client.query.column import Like + +account = iRODSAccount( + 'localhost', + 1247, + 'rods', + 'tempZone', + password='rods' +) + +conn = Connection( account ) + +# Negating individual columns will exclude them from the row results. Order is important. + +for row in Query( + conn, + ( DataObject, + Collection, + -DataObject.map_id, + -DataObject.status, + -Collection.map_id, + ) +).filter( Like(DataObject.name, 'a%'), ): + coll = row[Collection.name] + data = row[DataObject.name] + print( f'{row[DataObject.id]=} {row[Collection.id]=}: {coll}/{data}') diff --git a/fprc/irods/__init__.py b/fprc/irods/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fprc/irods/api_number.py b/fprc/irods/api_number.py new file mode 100644 index 000000000..af8fe277b --- /dev/null +++ b/fprc/irods/api_number.py @@ -0,0 +1,185 @@ +api_number = { + # 500 - 599 - Internal File I/O API calls + "FILE_CREATE_AN": 500, + "FILE_OPEN_AN": 501, + "FILE_WRITE_AN": 502, + "FILE_CLOSE_AN": 503, + "FILE_LSEEK_AN": 504, + "FILE_READ_AN": 505, + "FILE_UNLINK_AN": 506, + "FILE_MKDIR_AN": 507, + "FILE_CHMOD_AN": 508, + "FILE_RMDIR_AN": 509, + "FILE_STAT_AN": 510, + "FILE_FSTAT_AN": 511, + "FILE_FSYNC_AN": 512, + "FILE_STAGE_AN": 513, + "FILE_GET_FS_FREE_SPACE_AN": 514, + "FILE_OPENDIR_AN": 515, + "FILE_CLOSEDIR_AN": 516, + "FILE_READDIR_AN": 517, + "FILE_PUT_AN": 518, + "FILE_GET_AN": 519, + "FILE_CHKSUM_AN": 520, + "CHK_N_V_PATH_PERM_AN": 521, + "FILE_RENAME_AN": 522, + "FILE_TRUNCATE_AN": 523, + "FILE_STAGE_TO_CACHE_AN": 524, + "FILE_SYNC_TO_ARCH_AN": 525, + # 600 - 699 - Object File I/O API calls + "DATA_OBJ_CREATE_AN": 601, + "DATA_OBJ_OPEN_AN": 602, + "DATA_OBJ_PUT_AN": 606, + "DATA_PUT_AN": 607, + "DATA_OBJ_GET_AN": 608, + "DATA_GET_AN": 609, + "DATA_OBJ_REPL250_AN": 610, + "DATA_COPY_AN": 611, + "DATA_OBJ_COPY250_AN": 613, + "SIMPLE_QUERY_AN": 614, + "DATA_OBJ_UNLINK_AN": 615, + "REG_DATA_OBJ_AN": 619, + "UNREG_DATA_OBJ_AN": 620, + "REG_REPLICA_AN": 621, + "MOD_DATA_OBJ_META_AN": 622, + "RULE_EXEC_SUBMIT_AN": 623, + "RULE_EXEC_DEL_AN": 624, + "EXEC_MY_RULE_AN": 625, + "OPR_COMPLETE_AN": 626, + "DATA_OBJ_RENAME_AN": 627, + "DATA_OBJ_RSYNC_AN": 628, + "DATA_OBJ_CHKSUM_AN": 629, + "PHY_PATH_REG_AN": 630, + "DATA_OBJ_PHYMV250_AN": 631, + "DATA_OBJ_TRIM_AN": 632, + "OBJ_STAT_AN": 633, + "SUB_STRUCT_FILE_CREATE_AN": 635, + "SUB_STRUCT_FILE_OPEN_AN": 636, + "SUB_STRUCT_FILE_READ_AN": 637, + "SUB_STRUCT_FILE_WRITE_AN": 638, + "SUB_STRUCT_FILE_CLOSE_AN": 639, + "SUB_STRUCT_FILE_UNLINK_AN": 640, + "SUB_STRUCT_FILE_STAT_AN": 641, + "SUB_STRUCT_FILE_FSTAT_AN": 642, + "SUB_STRUCT_FILE_LSEEK_AN": 643, + "SUB_STRUCT_FILE_RENAME_AN": 644, + "QUERY_SPEC_COLL_AN": 645, + "SUB_STRUCT_FILE_MKDIR_AN": 647, + "SUB_STRUCT_FILE_RMDIR_AN": 648, + "SUB_STRUCT_FILE_OPENDIR_AN": 649, + "SUB_STRUCT_FILE_READDIR_AN": 650, + "SUB_STRUCT_FILE_CLOSEDIR_AN": 651, + "DATA_OBJ_TRUNCATE_AN": 652, + "SUB_STRUCT_FILE_TRUNCATE_AN": 653, + "GET_XMSG_TICKET_AN": 654, + "SEND_XMSG_AN": 655, + "RCV_XMSG_AN": 656, + "SUB_STRUCT_FILE_GET_AN": 657, + "SUB_STRUCT_FILE_PUT_AN": 658, + "SYNC_MOUNTED_COLL_AN": 659, + "STRUCT_FILE_SYNC_AN": 660, + "CLOSE_COLLECTION_AN": 661, + "STRUCT_FILE_EXTRACT_AN": 664, + "STRUCT_FILE_EXT_AND_REG_AN": 665, + "STRUCT_FILE_BUNDLE_AN": 666, + "CHK_OBJ_PERM_AND_STAT_AN": 667, + "GET_REMOTE_ZONE_RESC_AN": 668, + "DATA_OBJ_OPEN_AND_STAT_AN": 669, + "L3_FILE_GET_SINGLE_BUF_AN": 670, + "L3_FILE_PUT_SINGLE_BUF_AN": 671, + "DATA_OBJ_CREATE_AND_STAT_AN": 672, + "DATA_OBJ_CLOSE_AN": 673, + "DATA_OBJ_LSEEK_AN": 674, + "DATA_OBJ_READ_AN": 675, + "DATA_OBJ_WRITE_AN": 676, + "COLL_REPL_AN": 677, + "OPEN_COLLECTION_AN": 678, + "RM_COLL_AN": 679, + "MOD_COLL_AN": 680, + "COLL_CREATE_AN": 681, + "RM_COLL_OLD_AN": 682, + "REG_COLL_AN": 683, + "PHY_BUNDLE_COLL_AN": 684, + "UNBUN_AND_REG_PHY_BUNFILE_AN": 685, + "GET_HOST_FOR_PUT_AN": 686, + "GET_RESC_QUOTA_AN": 687, + "BULK_DATA_OBJ_REG_AN": 688, + "BULK_DATA_OBJ_PUT_AN": 689, + "PROC_STAT_AN": 690, + "STREAM_READ_AN": 691, + "EXEC_CMD_AN": 692, + "STREAM_CLOSE_AN": 693, + "GET_HOST_FOR_GET_AN": 694, + "DATA_OBJ_REPL_AN": 695, + "DATA_OBJ_COPY_AN": 696, + "DATA_OBJ_PHYMV_AN": 697, + "DATA_OBJ_FSYNC_AN": 698, + "DATA_OBJ_LOCK_AN": 699, + # 700 - 799 - Metadata API calls + "GET_MISC_SVR_INFO_AN": 700, + "GENERAL_ADMIN_AN": 701, + "GEN_QUERY_AN": 702, + "AUTH_REQUEST_AN": 703, + "AUTH_RESPONSE_AN": 704, + "AUTH_CHECK_AN": 705, + "MOD_AVU_METADATA_AN": 706, + "MOD_ACCESS_CONTROL_AN": 707, + "RULE_EXEC_MOD_AN": 708, + "GET_TEMP_PASSWORD_AN": 709, + "GENERAL_UPDATE_AN": 710, + "GSI_AUTH_REQUEST_AN": 711, + "READ_COLLECTION_AN": 713, + "USER_ADMIN_AN": 714, + "GENERAL_ROW_INSERT_AN": 715, + "GENERAL_ROW_PURGE_AN": 716, + "KRB_AUTH_REQUEST_AN": 717, + "END_TRANSACTION_AN": 718, + "DATABASE_RESC_OPEN_AN": 719, + "DATABASE_OBJ_CONTROL_AN": 720, + "DATABASE_RESC_CLOSE_AN": 721, + "SPECIFIC_QUERY_AN": 722, + "TICKET_ADMIN_AN": 723, + "GET_TEMP_PASSWORD_FOR_OTHER_AN": 724, + "PAM_AUTH_REQUEST_AN": 725, + "EXEC_CMD241_AN": 634, + "DATA_OBJ_READ201_AN": 603, + "DATA_OBJ_WRITE201_AN": 604, + "DATA_OBJ_CLOSE201_AN": 605, + "DATA_OBJ_LSEEK201_AN": 612, + "RM_COLL_OLD201_AN": 617, + "REG_COLL201_AN": 618, + "MOD_COLL201_AN": 646, + "COLL_REPL201_AN": 662, + "RM_COLL201_AN": 663, + "OPEN_COLLECTION201_AN": 712, + "GET_LIBRARY_FEATURES_AN": 801, + "REPLICA_TRUNCATE_AN": 802, + # 1000 - 1059 - NETCDF API calls + "NC_OPEN_AN": 1000, + "NC_CREATE_AN": 1001, + "NC_CLOSE_AN": 1002, + "NC_INQ_ID_AN": 1003, + "NC_INQ_WITH_ID_AN": 1004, + "NC_GET_VARS_BY_TYPE_AN": 1005, + "NCCF_GET_VARA_AN": 1006, + "NC_INQ_AN": 1007, + "NC_OPEN_GROUP_AN": 1008, + "NC_INQ_GRPS_AN": 1009, + "NC_REG_GLOBAL_ATTR_AN": 1010, + # 1060 - 1099 - OOI API calls + "OOI_GEN_SERV_REQ_AN": 1060, + # 1100 - 1200 - SSL API calls + "SSL_START_AN": 1100, + "SSL_END_AN": 1101, + "CLIENT_HINTS_AN": 10215, + "GET_RESOURCE_INFO_FOR_OPERATION_AN": 10220, + "GENQUERY2_AN": 10221, + "ATOMIC_APPLY_METADATA_OPERATIONS_APN": 20002, + "GET_FILE_DESCRIPTOR_INFO_APN": 20000, + "REPLICA_CLOSE_APN": 20004, + "ATOMIC_APPLY_ACL_OPERATIONS_APN": 20005, + "TOUCH_APN": 20007, + "AUTH_PLUG_REQ_AN": 1201, + "AUTHENTICATION_APN": 110000, + "ZONE_REPORT_AN": 10205, +} diff --git a/fprc/irods/client/__init__.py b/fprc/irods/client/__init__.py new file mode 100644 index 000000000..3d8e0b61e --- /dev/null +++ b/fprc/irods/client/__init__.py @@ -0,0 +1,5 @@ +#MAX_PASSWORD_LENGTH = 50 +#LONG_NAME_LEN = 256 +#MAX_PATH_ALLOWED = 1024 +#MAX_NAME_LEN = MAX_PATH_ALLOWED + 64 + diff --git a/fprc/irods/client/account.py b/fprc/irods/client/account.py new file mode 100644 index 000000000..a79038b00 --- /dev/null +++ b/fprc/irods/client/account.py @@ -0,0 +1,44 @@ +class iRODSAccount: + + def __init__( + self, + irods_host, + irods_port, + irods_user_name, + irods_zone_name, + irods_authentication_scheme="native", + password=None, + client_user=None, + server_dn=None, + client_zone=None, + **kwargs, + ): + + # For new auth framework (not yet used in FPRC) + tuplify = lambda _: _ if isinstance(_, (list, tuple)) else (_,) + schemes = [_.lower() for _ in tuplify(irods_authentication_scheme)] + self._original_authentication_scheme = schemes[-1] + self.authentication_scheme = schemes[0] + + # Initialize main fields for account. + self.host = irods_host + self.port = int(irods_port) + self.proxy_user = self.client_user = irods_user_name + self.proxy_zone = self.client_zone = irods_zone_name + self.server_dn = server_dn + self.password = password + + # Initialize other fields for account, as given in the kwargs parameter. + for key, value in kwargs.items(): + try: + if key.startswith("irods_"): + setattr(self, key[6:], value) + else: + setattr(self, key, value) + except TypeError: + setattr(self, key, value) + + if client_user: + self.client_user = client_user + if client_zone: + self.client_zone = client_zone diff --git a/fprc/irods/client/connection.py b/fprc/irods/client/connection.py new file mode 100644 index 000000000..1d23c2bbb --- /dev/null +++ b/fprc/irods/client/connection.py @@ -0,0 +1,578 @@ +from ast import literal_eval as safe_eval +import socket +import logging +import struct +import hashlib +import os +import re +import ssl +import datetime +import errno +from .utility import password_obfuscation as obf + +from ..server_constants import LONG_NAME_LEN, MAX_NAME_LEN, MAX_PASSWORD_LENGTH + +#from irods.exception import PAM_AUTH_PASSWORD_INVALID_TTL + +from .low_level.message import ( + iRODSMessage, + StartupPack, + AuthResponse, + AuthChallenge, + AuthPluginOut, + OpenedDataObjRequest, + FileSeekResponse, + StringStringMap, + VersionResponse, + PluginAuthMessage, + ClientServerNegotiation, + Error, + GetTempPasswordOut, +) + +from .low_level.exception import get_exception_by_code, NetworkException, nominal_code +from .low_level import exception as ex +from .low_level.message import PamAuthRequest, PamAuthRequestOut +# +# +## Message to be logged when the connection +## destructor is called. Used in a unit test +DESTRUCTOR_MSG = "connection __del__() called" + +from ..server_constants import RESPONSE_LEN +from ..connect_constants import NATIVE_AUTH_SCHEME + +from .low_level.client_server_negotiation import ( + perform_negotiation, + validate_policy, + REQUEST_NEGOTIATION, + FAILURE, + USE_SSL, + REQUIRE_TCP, + CS_NEG_RESULT_KW +) +from ..api_number import api_number +# +logger = logging.getLogger(__name__) +# +# + +class Connection: + def __init__(self, account, authenticate=True, application_name='FPRC'): + self.application_name = application_name + self.socket = None + self.account = account + self.auth_options = {} + self._client_signature = None + self._server_version = self._connect(authenticate) + self._disconnected = False + self.authenticated = False + + try: + + if not authenticate: + return + scheme = self.account._original_authentication_scheme + +# ------------- DWM temporarily commented out ------------- +# +# ses = self.pool.session_ref() +# if ses: +# ses.resolve_auth_options(scheme, conn=self) +# +# # These variables are just useful diagnostics. The login_XYZ() methods should fail by +# # raising exceptions if they encounter authentication errors. +# auth_module = auth_type = "" +# +# if self.server_version >= (4, 3, 0) and not cfg.legacy_auth.force_legacy_auth: +# import irods.auth +# +# auth_module = None +# # use client side "plugin" module: irods.auth. +# irods.auth.load_plugins(subset=[scheme]) +# auth_module = getattr(irods.auth, scheme, None) +# if auth_module: +# auth_module.login(self, **self.auth_options) +# auth_type = auth_module.__name__ +# else: +# ### - LEGACY pre-4.3 authentication + + if 1: #DWM + auth_type = scheme + if scheme == NATIVE_AUTH_SCHEME: + self._login_native() + else: + auth_type = None +# + + if auth_type: + self.authenticated = True + else: + msg = f"Authentication failed: scheme = {scheme!r}, auth_type = {auth_type!r}, auth_module = {auth_module!r}, " + raise ValueError(msg) + finally: + self.create_time = datetime.datetime.now() + self.last_used_time = self.create_time + + @property + def server_version(self): + detected = tuple(int(x) for x in self._server_version.relVersion.replace("rods", "").split(".")) + return detected +# return safe_eval(os.environ.get("IRODS_SERVER_VERSION", "()")) or detected +# + @property + def client_signature(self): + return self._client_signature + + def __del__(self): + try: + self.disconnect() + except Exception as e: + logger.info("Exception in finalizer: [%r]", e) + logger.debug(DESTRUCTOR_MSG) + + def send(self, message): + string = message.pack() + + logger.debug(string) + try: + self.socket.sendall(string) + except: + logger.error( + "Unable to send message. " + + "Connection to remote host may have closed. " + + "Releasing connection from pool." + ) + self.release(True) + raise NetworkException("Unable to send message") + + def recv(self, into_buffer=None, return_message=(), acceptable_errors=()): + acceptable_codes = set(nominal_code(e) for e in acceptable_errors) + try: + if into_buffer is None: + msg = iRODSMessage.recv(self.socket) + else: + msg = iRODSMessage.recv_into(self.socket, into_buffer) + except (socket.error, socket.timeout) as e: + # If _recv_message_in_len() fails in recv() or recv_into(), + # it will throw a socket.error exception. The exception is + # caught here, a critical message is logged, and is wrapped + # in a NetworkException with a more user friendly message + logger.critical(e) + logger.error("Could not receive server response") + self.release(True) + raise NetworkException("Could not receive server response") + if isinstance(return_message, list): + return_message[:] = [msg] + if msg.int_info < 0: + try: + err_msg = iRODSMessage(msg=msg.error).get_main_message(Error).RErrMsg_PI[0].msg + except TypeError: + err_msg = None + if nominal_code(msg.int_info) not in acceptable_codes: + exc = get_exception_by_code(msg.int_info, err_msg) + exc.server_msg = msg + raise exc + return msg + + def recv_into(self, buffer, **options): + return self.recv(into_buffer=buffer, **options) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.release() + + def release(self, destroy=False): + self.pool.release_connection(self, destroy) + + def reply(self, api_reply_index): + value = socket.htonl(api_reply_index) + try: + self.socket.sendall(struct.pack("I", value)) + except: + self.release(True) + raise NetworkException("Unable to send API reply") +# + def requires_cs_negotiation(self): + try: + if self.account.client_server_negotiation == REQUEST_NEGOTIATION: + return True + except AttributeError: + return False + return False +# +# @staticmethod +# def make_ssl_context(irods_account): +# verify_server = getattr(irods_account, "ssl_verify_server", "hostname") +# CAfile = getattr(irods_account, "ssl_ca_certificate_file", None) +# CApath = getattr(irods_account, "ssl_ca_certificate_path", None) +# verify = ssl.CERT_NONE if ((None is CAfile is CApath) or verify_server == "none") else ssl.CERT_REQUIRED +# # See https://stackoverflow.com/questions/30461969/disable-default-certificate-verification-in-python-2-7-9/49040695#49040695 +# ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=CAfile, capath=CApath) +# # Note: check_hostname must be assigned prior to verify_mode property or Python library complains! +# ctx.check_hostname = verify_server == "hostname" and verify != ssl.CERT_NONE +# ctx.verify_mode = verify +# return ctx +# +# def ssl_startup(self): +# # Get encryption settings from client environment +# host = self.account.host +# algo = self.account.encryption_algorithm +# key_size = self.account.encryption_key_size +# hash_rounds = self.account.encryption_num_hash_rounds +# salt_size = self.account.encryption_salt_size +# +# try: +# context = self.account.ssl_context +# except AttributeError: +# self.account.ssl_context = context = self.make_ssl_context(self.account) +# +# # Wrap socket with context +# wrapped_socket = context.wrap_socket(self.socket, server_hostname=(host if context.check_hostname else None)) +# +# # Initial SSL handshake +# wrapped_socket.do_handshake() +# +# # Generate key (shared secret) +# key = os.urandom(self.account.encryption_key_size) +# +# # Send header-only message with client side encryption settings +# packed_header = iRODSMessage.pack_header(algo, key_size, salt_size, hash_rounds, 0) +# wrapped_socket.sendall(packed_header) +# +# # Send shared secret +# packed_header = iRODSMessage.pack_header("SHARED_SECRET", key_size, 0, 0, 0) +# wrapped_socket.sendall(packed_header + key) +# +# # Use SSL socket from now on +# self.socket = wrapped_socket +# + def _connect(self, authenticate=True): + address = (self.account.host, self.account.port) + #timeout = self.pool.connection_timeout + + try: + s = socket.create_connection(address, + #timeout + ) + self._disconnected = False + except socket.error: + raise NetworkException("Could not connect to specified host and port: " + "{}:{}".format(*address)) + + self.socket = s + + main_message = StartupPack( + (self.account.proxy_user, self.account.proxy_zone), + (self.account.client_user, self.account.client_zone), + self.application_name, + ) + + # No client-server negotiation + if not self.requires_cs_negotiation(): + if len(main_message.option) >= LONG_NAME_LEN: + message = "Application name too long." + raise ValueError(message) + # Send startup pack without negotiation request + msg = iRODSMessage(msg_type="RODS_CONNECT", msg=main_message) + self.send(msg) + + # Server responds with version + version_msg = self.recv() + + # Done + return version_msg.get_main_message(VersionResponse) + + # Get client negotiation policy + client_policy = getattr(self.account, "client_server_policy", REQUIRE_TCP) + + # Sanity check + validate_policy(client_policy) + + # Send startup pack with negotiation request + main_message.option = "{}{}".format(main_message.option, REQUEST_NEGOTIATION) + if len(main_message.option) >= LONG_NAME_LEN: + message = f"Application name too long when appended with string {REQUEST_NEGOTIATION!r}." + raise ValueError(message) + msg = iRODSMessage(msg_type="RODS_CONNECT", msg=main_message) + self.send(msg) + + # Server responds with its own negotiation policy + cs_neg_msg = self.recv() + response = cs_neg_msg.get_main_message(ClientServerNegotiation) + server_policy = response.result + + # Perform the negotiation + neg_result, status = perform_negotiation(client_policy=client_policy, server_policy=server_policy) + + # Send negotiation result to server + client_neg_response = ClientServerNegotiation( + status=status, result="{}={};".format(CS_NEG_RESULT_KW, neg_result) + ) + msg = iRODSMessage(msg_type="RODS_CS_NEG_T", msg=client_neg_response) + self.send(msg) + + # If negotiation failed we're done + if neg_result == FAILURE: + self.disconnect() + raise NetworkException("Client-Server negotiation failure: {},{}".format(client_policy, server_policy)) + + # Server responds with version + version_msg = self.recv() + + if neg_result == USE_SSL: + self.ssl_startup() + + return version_msg.get_main_message(VersionResponse) + + def disconnect(self): + # Moved the conditions to call disconnect() inside the function. + # Added a new criteria for calling disconnect(); Only call + # disconnect() if fileno is not -1 (fileno -1 indicates the socket + # is already closed). This makes it safe to call disconnect multiple + # times on the same connection. The first call cleans up the resources + # and next calls are no-ops + try: + if self.socket and getattr(self, "_disconnected", False) == False and self.socket.fileno() != -1: + disconnect_msg = iRODSMessage(msg_type="RODS_DISCONNECT") + self.send(disconnect_msg) + try: + # SSL shutdown handshake + self.socket = self.socket.unwrap() + except AttributeError: + pass + try: + # BSD/MacOS calls to socket.shutdown() only succeed + # if the socket is still open. Linux and Windows do + # not care. So this call may fail sometimes if the + # socket is already closed/disconnected by the OS. + # OSError: [Errno 57] Socket is not connected + self.socket.shutdown(socket.SHUT_RDWR) + except OSError as e: + # Re-raise if this error is not the known BSD/MacOS error + if e.errno != errno.ENOTCONN: + raise + self.socket.close() + finally: + self._disconnected = True # Issue 368 - because of undefined destruction order during interpreter shutdown, + self.socket = None # as well as the fact that unhandled exceptions are ignored in __del__, we'd at least + # like to ensure as much cleanup as possible, thus preventing the above socket shutdown + # procedure from running too many times and creating confusing messages +# +# def recvall(self, n): +# # Helper function to recv n bytes or return None if EOF is hit +# data = b"" +# while len(data) < n: +# packet = self.socket.recv(n - len(data)) +# if not packet: +# return None +# data += packet +# return data +# +# def init_sec_context(self): +# import gssapi +# +# # AUTHORIZATION MECHANISM +# gsi_mech = gssapi.raw.OID.from_int_seq(GSI_OID) +# +# # SERVER NAME +# server_name = gssapi.Name(self.account.server_dn) +# server_name.canonicalize(gsi_mech) +# +# # CLIENT CONTEXT +# self.client_ctx = gssapi.SecurityContext(name=server_name, mech=gsi_mech, flags=[2, 4], usage="initiate") +# +# def send_gsi_token(self, server_token=None): +# +# # CLIENT TOKEN +# if server_token is None: +# client_token = self.client_ctx.step() +# else: +# client_token = self.client_ctx.step(server_token) +# logger.debug("[GSI handshake] Client: sent a new token") +# +# # SEND IT TO SERVER +# self.reply(len(client_token)) +# self.socket.sendall(client_token) +# +# def receive_gsi_token(self): +# +# # Receive client token from iRODS +# data = self.socket.recv(4) +# value = struct.unpack("I", bytearray(data)) +# token_len = socket.ntohl(value[0]) +# server_token = self.recvall(token_len) +# logger.debug("[GSI handshake] Server: received a new token") +# +# return server_token +# +# def handshake(self, target): +# """ +# This GSS API context based on GSI was obtained combining 2 sources: +# https://pythonhosted.org/gssapi/basic-tutorial.html +# https://github.com/irods/irods_auth_plugin_gsi/blob/master/gsi/libgsi.cpp +# """ +# +# self.init_sec_context() +# +# # Go, into the loop +# self.send_gsi_token() +# +# while not (self.client_ctx.complete): +# server_token = self.receive_gsi_token() +# +# self.send_gsi_token(server_token) +# +# logger.debug("[GSI Handshake] completed") +# +# def gsi_client_auth_request(self): +# +# # Request for authentication with GSI on current user +# +# message_body = PluginAuthMessage( +# auth_scheme_=GSI_AUTH_PLUGIN, +# context_="%s=%s" % (AUTH_USER_KEY, self.account.client_user), +# ) +# # GSI = 1201 +# # https://github.com/irods/irods/blob/master/lib/api/include/apiNumber.h#L158 +# auth_req = iRODSMessage(msg_type="RODS_API_REQ", msg=message_body, int_info=1201) +# self.send(auth_req) +# # Getting the challenge message +# self.recv() +# +# # This receive an empty message for confirmation... To check: +# # challenge_msg = self.recv() +# +# def read_file(self, desc, size=-1, buffer=None): +# print(f'{desc = }') +# if size < 0: +# size = len(buffer) +# elif buffer is not None: +# size = min(size, len(buffer)) +# #print('read_it') +# message_body = OpenedDataObjRequest( +# l1descInx=desc, +# len=size, +# whence=0, +# oprType=0, +# offset=0, +# bytesWritten=0, +# KeyValPair_PI=StringStringMap(), +# ) +# message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"]) +# +# logger.debug(desc) +# self.send(message) +# if buffer is None: +# response = self.recv() +# else: +# response = self.recv_into(buffer) +# +# return response.bs +# + def _login_native(self, password=None): + + # Default case, PAM login will send a new password + if password is None: + password = self.account.password or "" + + # authenticate + auth_req = iRODSMessage(msg_type="RODS_API_REQ", int_info=703) + self.send(auth_req) + + # challenge + challenge_msg = self.recv() + logger.debug(challenge_msg.msg) + challenge = challenge_msg.get_main_message(AuthChallenge).challenge + + # one "session" signature per connection + # see https://github.com/irods/irods/blob/4.2.1/plugins/auth/native/libnative.cpp#L137 + # and https://github.com/irods/irods/blob/4.2.1/lib/core/src/clientLogin.cpp#L38-L60 + self._client_signature = "".join("{:02x}".format(c) for c in challenge[:16]) + + challenge = challenge.strip() + padded_pwd = struct.pack("%ds" % MAX_PASSWORD_LENGTH, password.encode("utf-8").strip()) + + m = hashlib.md5() + m.update(challenge) + m.update(padded_pwd) + encoded_pwd = m.digest() + + if b"\x00" in encoded_pwd: + encoded_pwd_array = bytearray(encoded_pwd) + encoded_pwd = bytes(encoded_pwd_array.replace(b"\x00", b"\x01")) + + pwd_msg = AuthResponse(response=encoded_pwd, username=self.account.proxy_user) + pwd_request = iRODSMessage( + msg_type="RODS_API_REQ", + int_info=api_number["AUTH_RESPONSE_AN"], + msg=pwd_msg, + ) + self.send(pwd_request) + self.recv() + logger.info("Native authorization validated (in legacy auth).") + +# def write_file(self, desc, string): +# message_body = OpenedDataObjRequest( +# l1descInx=desc, +# len=len(string), +# whence=0, +# oprType=0, +# offset=0, +# bytesWritten=0, +# KeyValPair_PI=StringStringMap(), +# ) +# message = iRODSMessage( +# "RODS_API_REQ", +# msg=message_body, +# bs=string, +# int_info=api_number["DATA_OBJ_WRITE_AN"], +# ) +# self.send(message) +# response = self.recv() +# return response.int_info +# +# def seek_file(self, desc, offset, whence): +# message_body = OpenedDataObjRequest( +# l1descInx=desc, +# len=0, +# whence=whence, +# oprType=0, +# offset=offset, +# bytesWritten=0, +# KeyValPair_PI=StringStringMap(), +# ) +# message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"]) +# +# self.send(message) +# response = self.recv() +# offset = response.get_main_message(FileSeekResponse).offset +# return offset +# +# def close_file(self, desc, **options): +# message_body = OpenedDataObjRequest( +# l1descInx=desc, +# len=0, +# whence=0, +# oprType=0, +# offset=0, +# bytesWritten=0, +# KeyValPair_PI=StringStringMap(options), +# ) +# message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"]) +# +# self.send(message) +# self.recv() +# +# def temp_password(self): +# request = iRODSMessage("RODS_API_REQ", msg=None, int_info=api_number["GET_TEMP_PASSWORD_AN"]) +# +# # Send and receive request +# self.send(request) +# response = self.recv() +# logger.debug(response.int_info) +# +# # Convert and return answer +# msg = response.get_main_message(GetTempPasswordOut) +# return obf.create_temp_password(msg.stringToHashWith, self.account.password) diff --git a/fprc/irods/client/data/__init__.py b/fprc/irods/client/data/__init__.py new file mode 100644 index 000000000..3f8b44915 --- /dev/null +++ b/fprc/irods/client/data/__init__.py @@ -0,0 +1,139 @@ +from ...api_number import api_number +from ..low_level import keywords as kw + +from ..low_level.message import ( + FileOpenRequest, + FileSeekResponse, + iRODSMessage, + OpenedDataObjRequest, + StringStringMap, +) + +O_RDONLY = 0 +O_WRONLY = 1 +O_RDWR = 2 +O_APPEND = 1024 +O_CREAT = 64 +O_EXCL = 128 +O_TRUNC = 512 + +# TODO: This is suspect, since we don't know that iRODS and Linux agree on the value. +from os import SEEK_SET + +# TODO logging of XML + +def data_unlink(conn, /, path, force=False, **options): + # TODO: maybe leave it to caller to pass the iRODS flag in options. + if force: + options[kw.FORCE_FLAG_KW] = "" + + # TODO: Q: Is this for redundancy? + try: + oprType = options[kw.OPR_TYPE_KW] + except KeyError: + oprType = 0 + + message_body = FileOpenRequest( + objPath=path, + createMode=0, + openFlags=0, + offset=0, + dataSize=-1, + # TODO: Q: why have numThreads != 1 for an UNLINK? + numThreads=1, + oprType=oprType, + KeyValPair_PI=StringStringMap(options), + ) + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_UNLINK_AN"]) + conn.send(message) + _ = conn.recv() + +def data_open(conn, /, path, flags=O_RDONLY, **options): + message_body = FileOpenRequest( + objPath=path, + createMode=0o644, + openFlags=flags, + offset=0, + dataSize=-1, + numThreads=1, + oprType=0, + KeyValPair_PI=StringStringMap(options), + ) + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CREATE_AN"]) + conn.send(message) + response = conn.recv() + return response.int_info + +def g(): pass +def data_seek(conn, /, desc, offset, whence=SEEK_SET): + message_body = OpenedDataObjRequest( + l1descInx=desc, + len=0, + whence=whence, + oprType=0, + offset=offset, + bytesWritten=0, + KeyValPair_PI=StringStringMap(), + ) + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_LSEEK_AN"]) + + conn.send(message) + response = conn.recv() + offset = response.get_main_message(FileSeekResponse).offset + return offset + +def data_read(conn, /, desc, size=-1, buffer=None): + if size < 0: + size = len(buffer) + elif buffer is not None: + size = min(size, len(buffer)) + message_body = OpenedDataObjRequest( + l1descInx=desc, + len=size, + whence=0, + oprType=0, + offset=0, + bytesWritten=0, + KeyValPair_PI=StringStringMap(), + ) + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_READ_AN"]) + conn.send(message) + if buffer is None: + response = conn.recv() + else: + response = conn.recv_into(buffer) + return response.bs + +def data_write(conn, /, desc, string): + message_body = OpenedDataObjRequest( + l1descInx=desc, + len=len(string), + whence=0, + oprType=0, + offset=0, + bytesWritten=0, + KeyValPair_PI=StringStringMap(), + ) + message = iRODSMessage( + "RODS_API_REQ", + msg=message_body, + bs=string, + int_info=api_number["DATA_OBJ_WRITE_AN"], + ) + conn.send(message) + response = conn.recv() + return response.int_info + +def data_close(conn, /, desc, **options): + message_body = OpenedDataObjRequest( + l1descInx=desc, + len=0, + whence=0, + oprType=0, + offset=0, + bytesWritten=0, + KeyValPair_PI=StringStringMap(options), + ) + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["DATA_OBJ_CLOSE_AN"]) + conn.send(message) + conn.recv() diff --git a/fprc/irods/client/low_level/__init__.py b/fprc/irods/client/low_level/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fprc/irods/client/low_level/client_server_negotiation.py b/fprc/irods/client/low_level/client_server_negotiation.py new file mode 100644 index 000000000..e9d539916 --- /dev/null +++ b/fprc/irods/client/low_level/client_server_negotiation.py @@ -0,0 +1,31 @@ +# https://github.com/irods/irods/blob/4.2.1/lib/core/include/irods_client_server_negotiation.hpp +# https://github.com/irods/irods/blob/4.2.1/lib/core/src/irods_client_negotiation.cpp + +# Token sent to the server to request negotiation +REQUEST_NEGOTIATION = "request_server_negotiation" + +# Negotiation request values +REQUIRE_SSL = "CS_NEG_REQUIRE" +REQUIRE_TCP = "CS_NEG_REFUSE" + +# Negotiation result (response) values +FAILURE = "CS_NEG_FAILURE" +USE_SSL = "CS_NEG_USE_SSL" +USE_TCP = "CS_NEG_USE_TCP" + +# Keywords +CS_NEG_SID_KW = "cs_neg_sid_kw" +CS_NEG_RESULT_KW = "cs_neg_result_kw" + + +def perform_negotiation(client_policy, server_policy): + if REQUIRE_SSL in (client_policy, server_policy): + if REQUIRE_TCP in (client_policy, server_policy): + return FAILURE, 0 + return USE_SSL, 1 + return USE_TCP, 1 + + +def validate_policy(policy): + if policy not in (REQUIRE_SSL, REQUIRE_TCP): + raise ValueError("Invalid client-server negotiation policy: {}".format(policy)) diff --git a/fprc/irods/client/low_level/exception.py b/fprc/irods/client/low_level/exception.py new file mode 100644 index 000000000..8ee119771 --- /dev/null +++ b/fprc/irods/client/low_level/exception.py @@ -0,0 +1,2131 @@ +# if you're copying these from the docs, you might find the following regex helpful: +# s/\(\w\+\)\s\+\(-\d\+\)/class \1(SystemException):\r code = \2/g + +import errno +import numbers +import os +import sys +from typing import Dict + + +# ruff: noqa: N801 D101 off + + +class PycommandsException(Exception): + pass + + +class NetworkException(PycommandsException): + pass + + +class DoesNotExist(PycommandsException): + pass + + +class InvalidInputArgument(PycommandsException): + pass + + +class DataObjectExistsAtLogicalPath(PycommandsException): + pass + + +class DataObjectDoesNotExist(DoesNotExist): + pass + + +class CollectionDoesNotExist(DoesNotExist): + pass + + +class ZoneDoesNotExist(DoesNotExist): + pass + + +class UserDoesNotExist(DoesNotExist): + pass + + +class GroupDoesNotExist(DoesNotExist): + pass + + +class ResourceDoesNotExist(DoesNotExist): + pass + + +class OperationNotSupported(PycommandsException): + pass + + +class QueryException(PycommandsException): + pass + + +class NoResultFound(QueryException): + pass + + +class MultipleResultsFound(QueryException): + pass + + +class NotImplementedInIRODSServer(PycommandsException): + def __init__(self, feature_description, required_iRODS_version=()): + super(NotImplementedInIRODSServer, self).__init__( + feature_description + ": Not supported by the connected iRODS server." + ) + self.required_iRODS_version = required_iRODS_version + + def __str__(self): + nv = self.required_iRODS_version + return "{}{}".format( + self.args, + " [requires iRODS version: {nv}]".format(**locals()) if nv else "", + ) + + def __repr__(self): + return self.__class__.__name__ + str(self) + + +class iRODSExceptionMeta(type): + codes: "Dict[int, iRODSException]" = {} + positive_code_error_message = "For {name}, a positive code of {attrs[code]} was declared." + + def __init__(self, name, bases, attrs): + if "code" in attrs: + if attrs["code"] > 0: + print(self.positive_code_error_message.format(**locals()), file=sys.stderr) + exit(1) + iRODSExceptionMeta.codes[attrs["code"]] = self + + +class Errno: + """Encapsulates an integer error code from the operating system + and provides a text representation. + """ + + def __init__(self, arg0, *_): + """Initializes an object with an integer error code arg0. + Further arguments are optional and ignored. + """ + self.int_code = arg0 + + def __repr__(self): + e = self.int_code + try: + return self.__class__.__name__ + repr(tuple([e, errno.errorcode[e], os.strerror(e)])) + except: + # The errno code is unrecognized, so fall through to default representation. + pass + return self.__class__.__name__ + repr( + tuple([ + e, + ]) + ) + + def __int__(self): + return self.int_code + + +class iRODSException(Exception, metaclass=iRODSExceptionMeta): + """An exception that originates from a server error. + Exception classes that are derived from this base and represent a concrete error, should + store a unique error code (X*1000) in their 'code' attribute, where X < 0. + """ + + def __init__(self, *arg): + explicit_errno = None + argl = list(arg) + + # For consistency with the text representation, allow initialization with an Errno object + # at the end of the argument list. + # Example: err = UNIX_FILE_OPEN_ERR('message', Errno(13)) + # err_copy = eval(repr(err)) + if hasattr(self.__class__, "code"): + if argl and isinstance(argl[-1], Errno): + explicit_errno = argl.pop() + + super(iRODSException, self).__init__(*argl) + + # To properly represent the Errno instance, the "code" attribute should be (-errno) plus the thousands + # attribute stored in the class's code attribute. Because Python honors the instance "code" attribute + # over the class "code" attribute, the following does _not_ modify the class: + if explicit_errno: + self.code -= int(explicit_errno) + + def __repr__(self): + args = tuple(self.args) + code = getattr(self, "code", None) + if code is not None: + os_err = abs(code) % 1000 + if os_err: + args += (Errno(os_err),) + return self.__class__.__name__ + repr(args) + + +def nominal_code(the_code, THRESHOLD=1000): + nominal = [] + c = rounded_code(the_code, nominal_int_repo=nominal) + negated = -abs(nominal[0]) + return c if (negated <= -abs(THRESHOLD)) else negated # produce a negative for nonzero integer input + + +def rounded_code(the_code, nominal_int_repo=()): + nom_err = None + try: + if isinstance(the_code, type) and issubclass(the_code, iRODSException): + the_code = getattr(the_code, "code", the_code) + if isinstance(the_code, str): + nom_err = globals()[the_code].code + return nom_err + elif isinstance(the_code, numbers.Integral): + nom_err = the_code + return 1000 * ((-abs(the_code) - 1) // 1000 + 1) + else: + message = "Supplied code {the_code!r} must be integer or string".format(**locals()) + raise RuntimeError(message) + finally: + if nom_err is not None and isinstance(nominal_int_repo, list): + nominal_int_repo[:] = [nom_err] + + +def get_exception_class_by_code(code, name_only=False): + rounded = rounded_code(code) # rounded up to -1000 if code <= -1000 + cls = iRODSExceptionMeta.codes.get(rounded) + return cls if not name_only else (cls.__name__ if cls is not None else "Unknown_iRODS_error") + + +def get_exception_by_code(code, message=None): + exc_class = iRODSExceptionMeta.codes[rounded_code(code)] + exc_instance = exc_class(message) + exc_instance.code = code + return exc_instance + + +class UnknowniRODSError(iRODSException): + code = 0 # covers rounded_code (errcode) if 0 > errcode > -1000 + + +class SystemException(iRODSException): + pass + + +class SYS_SOCK_OPEN_ERR(SystemException): + code = -1000 + + +class SYS_SOCK_BIND_ERR(SystemException): + code = -2000 + + +class SYS_SOCK_ACCEPT_ERR(SystemException): + code = -3000 + + +class SYS_HEADER_READ_LEN_ERR(SystemException): + code = -4000 + + +class SYS_HEADER_WRITE_LEN_ERR(SystemException): + code = -5000 + + +class SYS_HEADER_TPYE_LEN_ERR(SystemException): + code = -6000 + + +class SYS_CAUGHT_SIGNAL(SystemException): + code = -7000 + + +class SYS_GETSTARTUP_PACK_ERR(SystemException): + code = -8000 + + +class SYS_EXCEED_CONNECT_CNT(SystemException): + code = -9000 + + +class SYS_USER_NOT_ALLOWED_TO_CONN(SystemException): + code = -10000 + + +class SYS_READ_MSG_BODY_INPUT_ERR(SystemException): + code = -11000 + + +class SYS_UNMATCHED_API_NUM(SystemException): + code = -12000 + + +class SYS_NO_API_PRIV(SystemException): + code = -13000 + + +class SYS_API_INPUT_ERR(SystemException): + code = -14000 + + +class SYS_PACK_INSTRUCT_FORMAT_ERR(SystemException): + code = -15000 + + +class SYS_MALLOC_ERR(SystemException): + code = -16000 + + +class SYS_GET_HOSTNAME_ERR(SystemException): + code = -17000 + + +class SYS_OUT_OF_FILE_DESC(SystemException): + code = -18000 + + +class SYS_FILE_DESC_OUT_OF_RANGE(SystemException): + code = -19000 + + +class SYS_UNRECOGNIZED_REMOTE_FLAG(SystemException): + code = -20000 + + +class SYS_INVALID_SERVER_HOST(SystemException): + code = -21000 + + +class SYS_SVR_TO_SVR_CONNECT_FAILED(SystemException): + code = -22000 + + +class SYS_BAD_FILE_DESCRIPTOR(SystemException): + code = -23000 + + +class SYS_INTERNAL_NULL_INPUT_ERR(SystemException): + code = -24000 + + +class SYS_CONFIG_FILE_ERR(SystemException): + code = -25000 + + +class SYS_INVALID_ZONE_NAME(SystemException): + code = -26000 + + +class SYS_COPY_LEN_ERR(SystemException): + code = -27000 + + +class SYS_PORT_COOKIE_ERR(SystemException): + code = -28000 + + +class SYS_KEY_VAL_TABLE_ERR(SystemException): + code = -29000 + + +class SYS_INVALID_RESC_TYPE(SystemException): + code = -30000 + + +class SYS_INVALID_FILE_PATH(SystemException): + code = -31000 + + +class SYS_INVALID_RESC_INPUT(SystemException): + code = -32000 + + +class SYS_INVALID_PORTAL_OPR(SystemException): + code = -33000 + + +class SYS_PARA_OPR_NO_SUPPORT(SystemException): + code = -34000 + + +class SYS_INVALID_OPR_TYPE(SystemException): + code = -35000 + + +class SYS_NO_PATH_PERMISSION(SystemException): + code = -36000 + + +class SYS_NO_ICAT_SERVER_ERR(SystemException): + code = -37000 + + +class SYS_AGENT_INIT_ERR(SystemException): + code = -38000 + + +class SYS_PROXYUSER_NO_PRIV(SystemException): + code = -39000 + + +class SYS_NO_DATA_OBJ_PERMISSION(SystemException): + code = -40000 + + +class SYS_DELETE_DISALLOWED(SystemException): + code = -41000 + + +class SYS_OPEN_REI_FILE_ERR(SystemException): + code = -42000 + + +class SYS_NO_RCAT_SERVER_ERR(SystemException): + code = -43000 + + +class SYS_UNMATCH_PACK_INSTRUCTI_NAME(SystemException): + code = -44000 + + +class SYS_SVR_TO_CLI_MSI_NO_EXIST(SystemException): + code = -45000 + + +class SYS_COPY_ALREADY_IN_RESC(SystemException): + code = -46000 + + +class SYS_RECONN_OPR_MISMATCH(SystemException): + code = -47000 + + +class SYS_INPUT_PERM_OUT_OF_RANGE(SystemException): + code = -48000 + + +class SYS_FORK_ERROR(SystemException): + code = -49000 + + +class SYS_PIPE_ERROR(SystemException): + code = -50000 + + +class SYS_EXEC_CMD_STATUS_SZ_ERROR(SystemException): + code = -51000 + + +class SYS_PATH_IS_NOT_A_FILE(SystemException): + code = -52000 + + +class SYS_UNMATCHED_SPEC_COLL_TYPE(SystemException): + code = -53000 + + +class SYS_TOO_MANY_QUERY_RESULT(SystemException): + code = -54000 + + +class SYS_SPEC_COLL_NOT_IN_CACHE(SystemException): + code = -55000 + + +class SYS_SPEC_COLL_OBJ_NOT_EXIST(SystemException): + code = -56000 + + +class SYS_REG_OBJ_IN_SPEC_COLL(SystemException): + code = -57000 + + +class SYS_DEST_SPEC_COLL_SUB_EXIST(SystemException): + code = -58000 + + +class SYS_SRC_DEST_SPEC_COLL_CONFLICT(SystemException): + code = -59000 + + +class SYS_UNKNOWN_SPEC_COLL_CLASS(SystemException): + code = -60000 + + +class SYS_DUPLICATE_XMSG_TICKET(SystemException): + code = -61000 + + +class SYS_UNMATCHED_XMSG_TICKET(SystemException): + code = -62000 + + +class SYS_NO_XMSG_FOR_MSG_NUMBER(SystemException): + code = -63000 + + +class SYS_COLLINFO_2_FORMAT_ERR(SystemException): + code = -64000 + + +class SYS_CACHE_STRUCT_FILE_RESC_ERR(SystemException): + code = -65000 + + +class SYS_NOT_SUPPORTED(SystemException): + code = -66000 + + +class SYS_TAR_STRUCT_FILE_EXTRACT_ERR(SystemException): + code = -67000 + + +class SYS_STRUCT_FILE_DESC_ERR(SystemException): + code = -68000 + + +class SYS_TAR_OPEN_ERR(SystemException): + code = -69000 + + +class SYS_TAR_EXTRACT_ALL_ERR(SystemException): + code = -70000 + + +class SYS_TAR_CLOSE_ERR(SystemException): + code = -71000 + + +class SYS_STRUCT_FILE_PATH_ERR(SystemException): + code = -72000 + + +class SYS_MOUNT_MOUNTED_COLL_ERR(SystemException): + code = -73000 + + +class SYS_COLL_NOT_MOUNTED_ERR(SystemException): + code = -74000 + + +class SYS_STRUCT_FILE_BUSY_ERR(SystemException): + code = -75000 + + +class SYS_STRUCT_FILE_INMOUNTED_COLL(SystemException): + code = -76000 + + +class SYS_COPY_NOT_EXIST_IN_RESC(SystemException): + code = -77000 + + +class SYS_RESC_DOES_NOT_EXIST(SystemException): + code = -78000 + + +class SYS_COLLECTION_NOT_EMPTY(SystemException): + code = -79000 + + +class SYS_OBJ_TYPE_NOT_STRUCT_FILE(SystemException): + code = -80000 + + +class SYS_WRONG_RESC_POLICY_FOR_BUN_OPR(SystemException): + code = -81000 + + +class SYS_DIR_IN_VAULT_NOT_EMPTY(SystemException): + code = -82000 + + +class SYS_OPR_FLAG_NOT_SUPPORT(SystemException): + code = -83000 + + +class SYS_TAR_APPEND_ERR(SystemException): + code = -84000 + + +class SYS_INVALID_PROTOCOL_TYPE(SystemException): + code = -85000 + + +class SYS_UDP_CONNECT_ERR(SystemException): + code = -86000 + + +class SYS_UDP_TRANSFER_ERR(SystemException): + code = -89000 + + +class SYS_UDP_NO_SUPPORT_ERR(SystemException): + code = -90000 + + +class SYS_READ_MSG_BODY_LEN_ERR(SystemException): + code = -91000 + + +class CROSS_ZONE_SOCK_CONNECT_ERR(SystemException): + code = -92000 + + +class SYS_NO_FREE_RE_THREAD(SystemException): + code = -93000 + + +class SYS_BAD_RE_THREAD_INX(SystemException): + code = -94000 + + +class SYS_CANT_DIRECTLY_ACC_COMPOUND_RESC(SystemException): + code = -95000 + + +class SYS_SRC_DEST_RESC_COMPOUND_TYPE(SystemException): + code = -96000 + + +class SYS_CACHE_RESC_NOT_ON_SAME_HOST(SystemException): + code = -97000 + + +class SYS_NO_CACHE_RESC_IN_GRP(SystemException): + code = -98000 + + +class SYS_UNMATCHED_RESC_IN_RESC_GRP(SystemException): + code = -99000 + + +class SYS_CANT_MV_BUNDLE_DATA_TO_TRASH(SystemException): + code = -100000 + + +class SYS_CANT_MV_BUNDLE_DATA_BY_COPY(SystemException): + code = -101000 + + +class SYS_EXEC_TAR_ERR(SystemException): + code = -102000 + + +class SYS_CANT_CHKSUM_COMP_RESC_DATA(SystemException): + code = -103000 + + +class SYS_CANT_CHKSUM_BUNDLED_DATA(SystemException): + code = -104000 + + +class SYS_RESC_IS_DOWN(SystemException): + code = -105000 + + +class SYS_UPDATE_REPL_INFO_ERR(SystemException): + code = -106000 + + +class SYS_COLL_LINK_PATH_ERR(SystemException): + code = -107000 + + +class SYS_LINK_CNT_EXCEEDED_ERR(SystemException): + code = -108000 + + +class SYS_CROSS_ZONE_MV_NOT_SUPPORTED(SystemException): + code = -109000 + + +class SYS_RESC_QUOTA_EXCEEDED(SystemException): + code = -110000 + + +class SYS_SOCK_READ_TIMEDOUT(SystemException): + code = -115000 + + +class SYS_INVALID_INPUT_PARAM(SystemException): + code = -130000 + + +class SYS_INTERNAL_ERR(SystemException): + code = -154000 + + +class SYS_BAD_INPUT(iRODSException): + code = -158000 + + +class SYS_REPLICA_DOES_NOT_EXIST(iRODSException): + code = -164000 + + +class SYS_LIBRARY_ERROR(iRODSException): + code = -167000 + + +class SYS_REPLICA_INACCESSIBLE(iRODSException): + code = -168000 + + +class SYS_NOT_ALLOWED(iRODSException): + code = -169000 + + +class UserInputException(iRODSException): + pass + + +class USER_AUTH_SCHEME_ERR(UserInputException): + code = -300000 + + +class USER_AUTH_STRING_EMPTY(UserInputException): + code = -301000 + + +class USER_RODS_HOST_EMPTY(UserInputException): + code = -302000 + + +class USER_RODS_HOSTNAME_ERR(UserInputException): + code = -303000 + + +class USER_SOCK_OPEN_ERR(UserInputException): + code = -304000 + + +class USER_SOCK_CONNECT_ERR(UserInputException): + code = -305000 + + +class USER_STRLEN_TOOLONG(UserInputException): + code = -306000 + + +class USER_API_INPUT_ERR(UserInputException): + code = -307000 + + +class USER_PACKSTRUCT_INPUT_ERR(UserInputException): + code = -308000 + + +class USER_NO_SUPPORT_ERR(UserInputException): + code = -309000 + + +class USER_FILE_DOES_NOT_EXIST(UserInputException): + code = -310000 + + +class USER_FILE_TOO_LARGE(UserInputException): + code = -311000 + + +class OVERWRITE_WITHOUT_FORCE_FLAG(UserInputException): + code = -312000 + + +class UNMATCHED_KEY_OR_INDEX(UserInputException): + code = -313000 + + +class USER_CHKSUM_MISMATCH(UserInputException): + code = -314000 + + +class USER_BAD_KEYWORD_ERR(UserInputException): + code = -315000 + + +class USER__NULL_INPUT_ERR(UserInputException): + code = -316000 + + +class USER_INPUT_PATH_ERR(UserInputException): + code = -317000 + + +class USER_INPUT_OPTION_ERR(UserInputException): + code = -318000 + + +class USER_INVALID_USERNAME_FORMAT(UserInputException): + code = -319000 + + +class USER_DIRECT_RESC_INPUT_ERR(UserInputException): + code = -320000 + + +class USER_NO_RESC_INPUT_ERR(UserInputException): + code = -321000 + + +class USER_PARAM_LABEL_ERR(UserInputException): + code = -322000 + + +class USER_PARAM_TYPE_ERR(UserInputException): + code = -323000 + + +class BASE64_BUFFER_OVERFLOW(UserInputException): + code = -324000 + + +class BASE64_INVALID_PACKET(UserInputException): + code = -325000 + + +class USER_MSG_TYPE_NO_SUPPORT(UserInputException): + code = -326000 + + +class USER_RSYNC_NO_MODE_INPUT_ERR(UserInputException): + code = -337000 + + +class USER_OPTION_INPUT_ERR(UserInputException): + code = -338000 + + +class SAME_SRC_DEST_PATHS_ERR(UserInputException): + code = -339000 + + +class USER_RESTART_FILE_INPUT_ERR(UserInputException): + code = -340000 + + +class RESTART_OPR_FAILED(UserInputException): + code = -341000 + + +class BAD_EXEC_CMD_PATH(UserInputException): + code = -342000 + + +class EXEC_CMD_OUTPUT_TOO_LARGE(UserInputException): + code = -343000 + + +class EXEC_CMD_ERROR(UserInputException): + code = -344000 + + +class BAD_INPUT_DESC_INDEX(UserInputException): + code = -345000 + + +class USER_PATH_EXCEEDS_MAX(UserInputException): + code = -346000 + + +class USER_SOCK_CONNECT_TIMEDOUT(UserInputException): + code = -347000 + + +class USER_API_VERSION_MISMATCH(UserInputException): + code = -348000 + + +class USER_INPUT_FORMAT_ERR(UserInputException): + code = -349000 + + +class USER_ACCESS_DENIED(UserInputException): + code = -350000 + + +class CANT_RM_MV_BUNDLE_TYPE(UserInputException): + code = -351000 + + +class NO_MORE_RESULT(UserInputException): + code = -352000 + + +class NO_KEY_WD_IN_MS_INP_STR(UserInputException): + code = -353000 + + +class CANT_RM_NON_EMPTY_HOME_COLL(UserInputException): + code = -354000 + + +class CANT_UNREG_IN_VAULT_FILE(UserInputException): + code = -355000 + + +class NO_LOCAL_FILE_RSYNC_IN_MSI(UserInputException): + code = -356000 + + +class OBJ_PATH_DOES_NOT_EXIST(iRODSException): + code = -358000 + + +class LOCKED_DATA_OBJECT_ACCESS(SystemException): + code = -406000 + + +class USER_INCOMPATIBLE_PARAMS(iRODSException): + code = -402000 + + +class CHECK_VERIFICATION_RESULTS(SystemException): + code = -407000 + + +class FileDriverException(iRODSException): + pass + + +class FILE_INDEX_LOOKUP_ERR(FileDriverException): + code = -500000 + + +class UNIX_FILE_OPEN_ERR(FileDriverException): + code = -510000 + + +class UNIX_FILE_CREATE_ERR(FileDriverException): + code = -511000 + + +class UNIX_FILE_READ_ERR(FileDriverException): + code = -512000 + + +class UNIX_FILE_WRITE_ERR(FileDriverException): + code = -513000 + + +class UNIX_FILE_CLOSE_ERR(FileDriverException): + code = -514000 + + +class UNIX_FILE_UNLINK_ERR(FileDriverException): + code = -515000 + + +class UNIX_FILE_STAT_ERR(FileDriverException): + code = -516000 + + +class UNIX_FILE_FSTAT_ERR(FileDriverException): + code = -517000 + + +class UNIX_FILE_LSEEK_ERR(FileDriverException): + code = -518000 + + +class UNIX_FILE_FSYNC_ERR(FileDriverException): + code = -519000 + + +class UNIX_FILE_MKDIR_ERR(FileDriverException): + code = -520000 + + +class UNIX_FILE_RMDIR_ERR(FileDriverException): + code = -521000 + + +class UNIX_FILE_OPENDIR_ERR(FileDriverException): + code = -522000 + + +class UNIX_FILE_CLOSEDIR_ERR(FileDriverException): + code = -523000 + + +class UNIX_FILE_READDIR_ERR(FileDriverException): + code = -524000 + + +class UNIX_FILE_STAGE_ERR(FileDriverException): + code = -525000 + + +class UNIX_FILE_GET_FS_FREESPACE_ERR(FileDriverException): + code = -526000 + + +class UNIX_FILE_CHMOD_ERR(FileDriverException): + code = -527000 + + +class UNIX_FILE_RENAME_ERR(FileDriverException): + code = -528000 + + +class UNIX_FILE_TRUNCATE_ERR(FileDriverException): + code = -529000 + + +class UNIX_FILE_LINK_ERR(FileDriverException): + code = -530000 + + +class UniversalMSSDriverException(FileDriverException): + pass + + +class UNIV_MSS_SYNCTOARCH_ERR(UniversalMSSDriverException): + code = -550000 + + +class UNIV_MSS_STAGETOCACHE_ERR(UniversalMSSDriverException): + code = -551000 + + +class UNIV_MSS_UNLINK_ERR(UniversalMSSDriverException): + code = -552000 + + +class UNIV_MSS_MKDIR_ERR(UniversalMSSDriverException): + code = -553000 + + +class UNIV_MSS_CHMOD_ERR(UniversalMSSDriverException): + code = -554000 + + +class UNIV_MSS_STAT_ERR(UniversalMSSDriverException): + code = -555000 + + +class HPSSDriverException(FileDriverException): + pass + + +class HPSS_AUTH_NOT_SUPPORTED(HPSSDriverException): + code = -600000 + + +class HPSS_FILE_OPEN_ERR(HPSSDriverException): + code = -610000 + + +class HPSS_FILE_CREATE_ERR(HPSSDriverException): + code = -611000 + + +class HPSS_FILE_READ_ERR(HPSSDriverException): + code = -612000 + + +class HPSS_FILE_WRITE_ERR(HPSSDriverException): + code = -613000 + + +class HPSS_FILE_CLOSE_ERR(HPSSDriverException): + code = -614000 + + +class HPSS_FILE_UNLINK_ERR(HPSSDriverException): + code = -615000 + + +class HPSS_FILE_STAT_ERR(HPSSDriverException): + code = -616000 + + +class HPSS_FILE_FSTAT_ERR(HPSSDriverException): + code = -617000 + + +class HPSS_FILE_LSEEK_ERR(HPSSDriverException): + code = -618000 + + +class HPSS_FILE_FSYNC_ERR(HPSSDriverException): + code = -619000 + + +class HPSS_FILE_MKDIR_ERR(HPSSDriverException): + code = -620000 + + +class HPSS_FILE_RMDIR_ERR(HPSSDriverException): + code = -621000 + + +class HPSS_FILE_OPENDIR_ERR(HPSSDriverException): + code = -622000 + + +class HPSS_FILE_CLOSEDIR_ERR(HPSSDriverException): + code = -623000 + + +class HPSS_FILE_READDIR_ERR(HPSSDriverException): + code = -624000 + + +class HPSS_FILE_STAGE_ERR(HPSSDriverException): + code = -625000 + + +class HPSS_FILE_GET_FS_FREESPACE_ERR(HPSSDriverException): + code = -626000 + + +class HPSS_FILE_CHMOD_ERR(HPSSDriverException): + code = -627000 + + +class HPSS_FILE_RENAME_ERR(HPSSDriverException): + code = -628000 + + +class HPSS_FILE_TRUNCATE_ERR(HPSSDriverException): + code = -629000 + + +class HPSS_FILE_LINK_ERR(HPSSDriverException): + code = -630000 + + +class HPSS_AUTH_ERR(HPSSDriverException): + code = -631000 + + +class HPSS_WRITE_LIST_ERR(HPSSDriverException): + code = -632000 + + +class HPSS_READ_LIST_ERR(HPSSDriverException): + code = -633000 + + +class HPSS_TRANSFER_ERR(HPSSDriverException): + code = -634000 + + +class HPSS_MOVER_PROT_ERR(HPSSDriverException): + code = -635000 + + +class AmazonS3Exception(FileDriverException): + pass + + +class S3_INIT_ERROR(AmazonS3Exception): + code = -701000 + + +class S3_PUT_ERROR(AmazonS3Exception): + code = -702000 + + +class S3_GET_ERROR(AmazonS3Exception): + code = -703000 + + +class S3_FILE_UNLINK_ERR(AmazonS3Exception): + code = -715000 + + +class S3_FILE_STAT_ERR(AmazonS3Exception): + code = -716000 + + +class S3_FILE_COPY_ERR(AmazonS3Exception): + code = -717000 + + +class CatalogLibraryException(iRODSException): + pass + + +class CATALOG_NOT_CONNECTED(CatalogLibraryException): + code = -801000 + + +class CAT_ENV_ERR(CatalogLibraryException): + code = -802000 + + +class CAT_CONNECT_ERR(CatalogLibraryException): + code = -803000 + + +class CAT_DISCONNECT_ERR(CatalogLibraryException): + code = -804000 + + +class CAT_CLOSE_ENV_ERR(CatalogLibraryException): + code = -805000 + + +class CAT_SQL_ERR(CatalogLibraryException): + code = -806000 + + +class CAT_GET_ROW_ERR(CatalogLibraryException): + code = -807000 + + +class CAT_NO_ROWS_FOUND(CatalogLibraryException): + code = -808000 + + +class CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME(CatalogLibraryException): + code = -809000 + + +class CAT_NO_CHECKSUM_FOR_REPLICA(CatalogLibraryException): + code = -862000 + + +class CAT_INVALID_RESOURCE_TYPE(CatalogLibraryException): + code = -810000 + + +class CAT_INVALID_RESOURCE_CLASS(CatalogLibraryException): + code = -811000 + + +class CAT_INVALID_RESOURCE_NET_ADDR(CatalogLibraryException): + code = -812000 + + +class CAT_INVALID_RESOURCE_VAULT_PATH(CatalogLibraryException): + code = -813000 + + +class CAT_UNKNOWN_COLLECTION(CatalogLibraryException): + code = -814000 + + +class CAT_INVALID_DATA_TYPE(CatalogLibraryException): + code = -815000 + + +class CAT_INVALID_ARGUMENT(CatalogLibraryException): + code = -816000 + + +class CAT_UNKNOWN_FILE(CatalogLibraryException): + code = -817000 + + +class CAT_NO_ACCESS_PERMISSION(CatalogLibraryException): + code = -818000 + + +class CAT_SUCCESS_BUT_WITH_NO_INFO(CatalogLibraryException): + code = -819000 + + +class CAT_INVALID_USER_TYPE(CatalogLibraryException): + code = -820000 + + +class CAT_COLLECTION_NOT_EMPTY(CatalogLibraryException): + code = -821000 + + +class CAT_TOO_MANY_TABLES(CatalogLibraryException): + code = -822000 + + +class CAT_UNKNOWN_TABLE(CatalogLibraryException): + code = -823000 + + +class CAT_NOT_OPEN(CatalogLibraryException): + code = -824000 + + +class CAT_FAILED_TO_LINK_TABLES(CatalogLibraryException): + code = -825000 + + +class CAT_INVALID_AUTHENTICATION(CatalogLibraryException): + code = -826000 + + +class CAT_INVALID_USER(CatalogLibraryException): + code = -827000 + + +class CAT_INVALID_ZONE(CatalogLibraryException): + code = -828000 + + +class CAT_INVALID_GROUP(CatalogLibraryException): + code = -829000 + + +class CAT_INSUFFICIENT_PRIVILEGE_LEVEL(CatalogLibraryException): + code = -830000 + + +class CAT_INVALID_RESOURCE(CatalogLibraryException): + code = -831000 + + +class CAT_INVALID_CLIENT_USER(CatalogLibraryException): + code = -832000 + + +class CAT_NAME_EXISTS_AS_COLLECTION(CatalogLibraryException): + code = -833000 + + +class CAT_NAME_EXISTS_AS_DATAOBJ(CatalogLibraryException): + code = -834000 + + +class CAT_RESOURCE_NOT_EMPTY(CatalogLibraryException): + code = -835000 + + +class CAT_NOT_A_DATAOBJ_AND_NOT_A_COLLECTION(CatalogLibraryException): + code = -836000 + + +class CAT_RECURSIVE_MOVE(CatalogLibraryException): + code = -837000 + + +class CAT_LAST_REPLICA(CatalogLibraryException): + code = -838000 + + +class CAT_OCI_ERROR(CatalogLibraryException): + code = -839000 + + +class CAT_PASSWORD_EXPIRED(CatalogLibraryException): + code = -840000 + + +class CAT_PASSWORD_ENCODING_ERROR(CatalogLibraryException): + code = -850000 + + +class CAT_TABLE_ACCESS_DENIED(CatalogLibraryException): + code = -851000 + + +class CAT_UNKNOWN_SPECIFIC_QUERY(CatalogLibraryException): + code = -853000 + + +class CAT_STATEMENT_TABLE_FULL(CatalogLibraryException): + code = -860000 + + +class RDSException(iRODSException): + pass + + +class RDA_NOT_COMPILED_IN(RDSException): + code = -880000 + + +class RDA_NOT_CONNECTED(RDSException): + code = -881000 + + +class RDA_ENV_ERR(RDSException): + code = -882000 + + +class RDA_CONNECT_ERR(RDSException): + code = -883000 + + +class RDA_DISCONNECT_ERR(RDSException): + code = -884000 + + +class RDA_CLOSE_ENV_ERR(RDSException): + code = -885000 + + +class RDA_SQL_ERR(RDSException): + code = -886000 + + +class RDA_CONFIG_FILE_ERR(RDSException): + code = -887000 + + +class RDA_ACCESS_PROHIBITED(RDSException): + code = -888000 + + +class RDA_NAME_NOT_FOUND(RDSException): + code = -889000 + + +class TicketException(CatalogLibraryException): + pass + + +class CAT_TICKET_INVALID(TicketException): + code = -890000 + + +class CAT_TICKET_EXPIRED(TicketException): + code = -891000 + + +class CAT_TICKET_USES_EXCEEDED(TicketException): + code = -892000 + + +class MiscException(iRODSException): + pass + + +class FILE_OPEN_ERR(MiscException): + code = -900000 + + +class FILE_READ_ERR(MiscException): + code = -901000 + + +class FILE_WRITE_ERR(MiscException): + code = -902000 + + +class PASSWORD_EXCEEDS_MAX_SIZE(MiscException): + code = -903000 + + +class ENVIRONMENT_VAR_HOME_NOT_DEFINED(MiscException): + code = -904000 + + +class UNABLE_TO_STAT_FILE(MiscException): + code = -905000 + + +class AUTH_FILE_NOT_ENCRYPTED(MiscException): + code = -906000 + + +class AUTH_FILE_DOES_NOT_EXIST(MiscException): + code = -907000 + + +class UNLINK_FAILED(MiscException): + code = -908000 + + +class NO_PASSWORD_ENTERED(MiscException): + code = -909000 + + +class REMOTE_SERVER_AUTHENTICATION_FAILURE(MiscException): + code = -910000 + + +class REMOTE_SERVER_AUTH_NOT_PROVIDED(MiscException): + code = -911000 + + +class REMOTE_SERVER_AUTH_EMPTY(MiscException): + code = -912000 + + +class REMOTE_SERVER_SID_NOT_DEFINED(MiscException): + code = -913000 + + +class GSIKRBException(iRODSException): + pass + + +class GSIException(GSIKRBException): + pass + + +class GSI_NOT_COMPILED_IN(GSIException): + code = -921000 + + +class GSI_NOT_BUILT_INTO_CLIENT(GSIException): + code = -922000 + + +class GSI_NOT_BUILT_INTO_SERVER(GSIException): + code = -923000 + + +class GSI_ERROR_IMPORT_NAME(GSIException): + code = -924000 + + +class GSI_ERROR_INIT_SECURITY_CONTEXT(GSIException): + code = -925000 + + +class GSI_ERROR_SENDING_TOKEN_LENGTH(GSIException): + code = -926000 + + +class GSI_ERROR_READING_TOKEN_LENGTH(GSIException): + code = -927000 + + +class GSI_ERROR_TOKEN_TOO_LARGE(GSIException): + code = -928000 + + +class GSI_ERROR_BAD_TOKEN_RCVED(GSIException): + code = -929000 + + +class GSI_SOCKET_READ_ERROR(GSIException): + code = -930000 + + +class GSI_PARTIAL_TOKEN_READ(GSIException): + code = -931000 + + +class GSI_SOCKET_WRITE_ERROR(GSIException): + code = -932000 + + +class GSI_ERROR_FROM_GSI_LIBRARY(GSIException): + code = -933000 + + +class GSI_ERROR_IMPORTING_NAME(GSIException): + code = -934000 + + +class GSI_ERROR_ACQUIRING_CREDS(GSIException): + code = -935000 + + +class GSI_ACCEPT_SEC_CONTEXT_ERROR(GSIException): + code = -936000 + + +class GSI_ERROR_DISPLAYING_NAME(GSIException): + code = -937000 + + +class GSI_ERROR_RELEASING_NAME(GSIException): + code = -938000 + + +class GSI_DN_DOES_NOT_MATCH_USER(GSIException): + code = -939000 + + +class GSI_QUERY_INTERNAL_ERROR(GSIException): + code = -940000 + + +class GSI_NO_MATCHING_DN_FOUND(GSIException): + code = -941000 + + +class GSI_MULTIPLE_MATCHING_DN_FOUND(GSIException): + code = -942000 + + +class KRBException(GSIKRBException): + pass + + +class KRB_NOT_COMPILED_IN(KRBException): + code = -951000 + + +class KRB_NOT_BUILT_INTO_CLIENT(KRBException): + code = -952000 + + +class KRB_NOT_BUILT_INTO_SERVER(KRBException): + code = -953000 + + +class KRB_ERROR_IMPORT_NAME(KRBException): + code = -954000 + + +class KRB_ERROR_INIT_SECURITY_CONTEXT(KRBException): + code = -955000 + + +class KRB_ERROR_SENDING_TOKEN_LENGTH(KRBException): + code = -956000 + + +class KRB_ERROR_READING_TOKEN_LENGTH(KRBException): + code = -957000 + + +class KRB_ERROR_TOKEN_TOO_LARGE(KRBException): + code = -958000 + + +class KRB_ERROR_BAD_TOKEN_RCVED(KRBException): + code = -959000 + + +class KRB_SOCKET_READ_ERROR(KRBException): + code = -960000 + + +class KRB_PARTIAL_TOKEN_READ(KRBException): + code = -961000 + + +class KRB_SOCKET_WRITE_ERROR(KRBException): + code = -962000 + + +class KRB_ERROR_FROM_KRB_LIBRARY(KRBException): + code = -963000 + + +class KRB_ERROR_IMPORTING_NAME(KRBException): + code = -964000 + + +class KRB_ERROR_ACQUIRING_CREDS(KRBException): + code = -965000 + + +class KRB_ACCEPT_SEC_CONTEXT_ERROR(KRBException): + code = -966000 + + +class KRB_ERROR_DISPLAYING_NAME(KRBException): + code = -967000 + + +class KRB_ERROR_RELEASING_NAME(KRBException): + code = -968000 + + +class KRB_USER_DN_NOT_FOUND(KRBException): + code = -969000 + + +class KRB_NAME_MATCHES_MULTIPLE_USERS(KRBException): + code = -970000 + + +class KRB_QUERY_INTERNAL_ERROR(KRBException): + code = -971000 + + +class RuleEngineException(iRODSException): + pass + + +class OBJPATH_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1000000 + + +class RESCNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1001000 + + +class DATATYPE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1002000 + + +class DATASIZE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1003000 + + +class CHKSUM_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1004000 + + +class VERSION_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1005000 + + +class FILEPATH_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1006000 + + +class REPLNUM_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1007000 + + +class REPLSTATUS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1008000 + + +class DATAOWNER_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1009000 + + +class DATAOWNERZONE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1010000 + + +class DATAEXPIRY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1011000 + + +class DATACOMMENTS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1012000 + + +class DATACREATE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1013000 + + +class DATAMODIFY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1014000 + + +class DATAACCESS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1015000 + + +class DATAACCESSINX_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1016000 + + +class RE_RUNTIME_ERROR(RuleEngineException): + code = -1205000 + + +class RE_TYPE_ERROR(RuleEngineException): + code = -1230000 + + +class NO_RULE_FOUND_ERR(RuleEngineException): + code = -1017000 + + +class NO_MORE_RULES_ERR(RuleEngineException): + code = -1018000 + + +class UNMATCHED_ACTION_ERR(RuleEngineException): + code = -1019000 + + +class RULES_FILE_READ_ERROR(RuleEngineException): + code = -1020000 + + +class ACTION_ARG_COUNT_MISMATCH(RuleEngineException): + code = -1021000 + + +class MAX_NUM_OF_ARGS_IN_ACTION_EXCEEDED(RuleEngineException): + code = -1022000 + + +class UNKNOWN_PARAM_IN_RULE_ERR(RuleEngineException): + code = -1023000 + + +class DESTRESCNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1024000 + + +class BACKUPRESCNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1025000 + + +class DATAID_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1026000 + + +class COLLID_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1027000 + + +class RESCGROUPNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1028000 + + +class STATUSSTRING_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1029000 + + +class DATAMAPID_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1030000 + + +class USERNAMECLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1031000 + + +class RODSZONECLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1032000 + + +class USERTYPECLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1033000 + + +class HOSTCLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1034000 + + +class AUTHSTRCLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1035000 + + +class USERAUTHSCHEMECLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1036000 + + +class USERINFOCLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1037000 + + +class USERCOMMENTCLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1038000 + + +class USERCREATECLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1039000 + + +class USERMODIFYCLIENT_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1040000 + + +class USERNAMEPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1041000 + + +class RODSZONEPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1042000 + + +class USERTYPEPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1043000 + + +class HOSTPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1044000 + + +class AUTHSTRPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1045000 + + +class USERAUTHSCHEMEPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1046000 + + +class USERINFOPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1047000 + + +class USERCOMMENTPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1048000 + + +class USERCREATEPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1049000 + + +class USERMODIFYPROXY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1050000 + + +class COLLNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1051000 + + +class COLLPARENTNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1052000 + + +class COLLOWNERNAME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1053000 + + +class COLLOWNERZONE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1054000 + + +class COLLEXPIRY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1055000 + + +class COLLCOMMENTS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1056000 + + +class COLLCREATE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1057000 + + +class COLLMODIFY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1058000 + + +class COLLACCESS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1059000 + + +class COLLACCESSINX_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1060000 + + +class COLLMAPID_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1062000 + + +class COLLINHERITANCE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1063000 + + +class RESCZONE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1065000 + + +class RESCLOC_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1066000 + + +class RESCTYPE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1067000 + + +class RESCTYPEINX_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1068000 + + +class RESCCLASS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1069000 + + +class RESCCLASSINX_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1070000 + + +class RESCVAULTPATH_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1071000 + + +class NUMOPEN_ORTS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1072000 + + +class PARAOPR_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1073000 + + +class RESCID_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1074000 + + +class GATEWAYADDR_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1075000 + + +class RESCMAX_BJSIZE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1076000 + + +class FREESPACE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1077000 + + +class FREESPACETIME_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1078000 + + +class FREESPACETIMESTAMP_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1079000 + + +class RESCINFO_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1080000 + + +class RESCCOMMENTS_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1081000 + + +class RESCCREATE_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1082000 + + +class RESCMODIFY_EMPTY_IN_STRUCT_ERR(RuleEngineException): + code = -1083000 + + +class INPUT_ARG_NOT_WELL_FORMED_ERR(RuleEngineException): + code = -1084000 + + +class INPUT_ARG_OUT_OF_ARGC_RANGE_ERR(RuleEngineException): + code = -1085000 + + +class INSUFFICIENT_INPUT_ARG_ERR(RuleEngineException): + code = -1086000 + + +class INPUT_ARG_DOES_NOT_MATCH_ERR(RuleEngineException): + code = -1087000 + + +class RETRY_WITHOUT_RECOVERY_ERR(RuleEngineException): + code = -1088000 + + +class CUT_ACTION_PROCESSED_ERR(RuleEngineException): + code = -1089000 + + +class ACTION_FAILED_ERR(RuleEngineException): + code = -1090000 + + +class FAIL_ACTION_ENCOUNTERED_ERR(RuleEngineException): + code = -1091000 + + +class VARIABLE_NAME_TOO_LONG_ERR(RuleEngineException): + code = -1092000 + + +class UNKNOWN_VARIABLE_MAP_ERR(RuleEngineException): + code = -1093000 + + +class UNDEFINED_VARIABLE_MAP_ERR(RuleEngineException): + code = -1094000 + + +class NULL_VALUE_ERR(RuleEngineException): + code = -1095000 + + +class DVARMAP_FILE_READ_ERROR(RuleEngineException): + code = -1096000 + + +class NO_RULE_OR_MSI_FUNCTION_FOUND_ERR(RuleEngineException): + code = -1097000 + + +class FILE_CREATE_ERROR(RuleEngineException): + code = -1098000 + + +class FMAP_FILE_READ_ERROR(RuleEngineException): + code = -1099000 + + +class DATE_FORMAT_ERR(RuleEngineException): + code = -1100000 + + +class RULE_FAILED_ERR(RuleEngineException): + code = -1101000 + + +class NO_MICROSERVICE_FOUND_ERR(RuleEngineException): + code = -1102000 + + +class INVALID_REGEXP(RuleEngineException): + code = -1103000 + + +class INVALID_OBJECT_NAME(RuleEngineException): + code = -1104000 + + +class INVALID_OBJECT_TYPE(RuleEngineException): + code = -1105000 + + +class NO_VALUES_FOUND(RuleEngineException): + code = -1106000 + + +class NO_COLUMN_NAME_FOUND(RuleEngineException): + code = -1107000 + + +class BREAK_ACTION_ENCOUNTERED_ERR(RuleEngineException): + code = -1108000 + + +class CUT_ACTION_ON_SUCCESS_PROCESSED_ERR(RuleEngineException): + code = -1109000 + + +class MSI_OPERATION_NOT_ALLOWED(RuleEngineException): + code = -1110000 + + +class RULE_ENGINE_ERROR(RuleEngineException): + code = -1828000 + + +class PHPException(iRODSException): + pass + + +class PHP_EXEC_SCRIPT_ERR(PHPException): + code = -1600000 + + +class PHP_REQUEST_STARTUP_ERR(PHPException): + code = -1601000 + + +class PHP_OPEN_SCRIPT_FILE_ERR(PHPException): + code = -1602000 + + +class HIERARCHY_ERROR(iRODSException): + code = -1803000 + + +class DIRECT_CHILD_ACCESS(iRODSException): + code = -1816000 + + +class PAMException(iRODSException): + pass + + +class PAM_AUTH_NOT_BUILT_INTO_CLIENT(PAMException): + code = -991000 + + +class PAM_AUTH_NOT_BUILT_INTO_SERVER(PAMException): + code = -992000 + + +class PAM_AUTH_PASSWORD_FAILED(PAMException): + code = -993000 + + +class PAM_AUTH_PASSWORD_INVALID_TTL(PAMException): + code = -994000 + + +# ruff: noqa: N801 D101 on diff --git a/fprc/irods/client/low_level/keywords.py b/fprc/irods/client/low_level/keywords.py new file mode 100644 index 000000000..1918ec19d --- /dev/null +++ b/fprc/irods/client/low_level/keywords.py @@ -0,0 +1,275 @@ +"""From rodsKeyWdDef.hpp""" + +ALL_KW = "all" # operation done on all replicas # +COPIES_KW = "copies" # the number of copies # +EXEC_LOCALLY_KW = "execLocally" # execute locally # +FORCE_FLAG_KW = "forceFlag" # force update # +CLI_IN_SVR_FIREWALL_KW = "cliInSvrFirewall" # cli behind same firewall # +REG_CHKSUM_KW = "regChksum" # register checksum # +VERIFY_CHKSUM_KW = "verifyChksum" # verify checksum # +NO_COMPUTE_KW = "no_compute" +VERIFY_BY_SIZE_KW = "verifyBySize" # verify by size - used by irsync # +OBJ_PATH_KW = "objPath" # logical path of the object # +RESC_NAME_KW = "rescName" # resource name # +DEST_RESC_NAME_KW = "destRescName" # destination resource name # +DEF_RESC_NAME_KW = "defRescName" # default resource name # +BACKUP_RESC_NAME_KW = "backupRescName" # backup resource name # +DATA_TYPE_KW = "dataType" # data type # +DATA_SIZE_KW = "dataSize" +CHKSUM_KW = "chksum" +ORIG_CHKSUM_KW = "orig_chksum" +VERSION_KW = "version" +FILE_PATH_KW = "filePath" # the physical file path # +BUN_FILE_PATH_KW = "bunFilePath" # the physical bun file path # # JMC - backport 4768 +REPL_NUM_KW = "replNum" # replica number # +WRITE_FLAG_KW = "writeFlag" # whether it is opened for write # +REPL_STATUS_KW = "replStatus" # status of the replica # +ALL_REPL_STATUS_KW = "allReplStatus" # update all replStatus # +METADATA_INCLUDED_KW = "metadataIncluded" # for atomic puts of data / metadata +ACL_INCLUDED_KW = "aclIncluded" # for atomic puts of data / access controls +DATA_INCLUDED_KW = "dataIncluded" # data included in the input buffer # +DATA_OWNER_KW = "dataOwner" +DATA_OWNER_ZONE_KW = "dataOwnerZone" +DATA_EXPIRY_KW = "dataExpiry" +DATA_COMMENTS_KW = "dataComments" +DATA_CREATE_KW = "dataCreate" +DATA_MODIFY_KW = "dataModify" +DATA_ACCESS_KW = "dataAccess" +DATA_ACCESS_INX_KW = "dataAccessInx" +DATA_ACCESS_TIME_KW = "dataAccessTime" +NO_OPEN_FLAG_KW = "noOpenFlag" +PHYOPEN_BY_SIZE_KW = "phyOpenBySize" +STREAMING_KW = "streaming" +DATA_ID_KW = "dataId" +COLL_ID_KW = "collId" +DATA_MODE_KW = "dataMode" +STATUS_STRING_KW = "statusString" +DATA_MAP_ID_KW = "dataMapId" +NO_PARA_OP_KW = "noParaOpr" +LOCAL_PATH_KW = "localPath" +RSYNC_MODE_KW = "rsyncMode" +RSYNC_DEST_PATH_KW = "rsyncDestPath" +RSYNC_CHKSUM_KW = "rsyncChksum" +CHKSUM_ALL_KW = "ChksumAll" +FORCE_CHKSUM_KW = "forceChksum" +COLLECTION_KW = "collection" +ADMIN_KW = "irodsAdmin" +ADMIN_RMTRASH_KW = "irodsAdminRmTrash" +UNREG_KW = "unreg" +RMTRASH_KW = "irodsRmTrash" +RECURSIVE_OPR__KW = "recursiveOpr" +COLLECTION_TYPE_KW = "collectionType" +COLLECTION_INFO1_KW = "collectionInfo1" +COLLECTION_INFO2_KW = "collectionInfo2" +SEL_OBJ_TYPE_KW = "selObjType" +STRUCT_FILE_OPR_KW = "structFileOpr" +ALL_MS_PARAM_KW = "allMsParam" +UNREG_COLL_KW = "unregColl" +UPDATE_REPL_KW = "updateRepl" +RBUDP_TRANSFER_KW = "rbudpTransfer" +VERY_VERBOSE_KW = "veryVerbose" +RBUDP_SEND_RATE_KW = "rbudpSendRate" +RBUDP_PACK_SIZE_KW = "rbudpPackSize" +ZONE_KW = "zone" +REMOTE_ZONE_OPR_KW = "remoteZoneOpr" +REPL_DATA_OBJ_INP_KW = "replDataObjInp" +CROSS_ZONE_CREATE_KW = "replDataObjInp" # use the same for backward compatibility # +QUERY_BY_DATA_ID_KW = "queryByDataID" +SU_CLIENT_USER_KW = "suClientUser" +RM_BUN_COPY_KW = "rmBunCopy" +KEY_WORD_KW = "keyWord" # the msKeyValStr is a keyword # +CREATE_MODE_KW = "createMode" # a msKeyValStr keyword # +OPEN_FLAGS_KW = "openFlags" # a msKeyValStr keyword # +OFFSET_KW = "offset" # a msKeyValStr keyword # +# DATA_SIZE_KW already defined # +NUM_THREADS_KW = "numThreads" # a msKeyValStr keyword # +OPR_TYPE_KW = "oprType" # a msKeyValStr keyword # +COLL_FLAGS_KW = "collFlags" # a msKeyValStr keyword # +TRANSLATED_PATH_KW = "translatedPath" # the path translated # +NO_TRANSLATE_LINKPT_KW = "noTranslateMntpt" # don't translate mntpt # +BULK_OPR_KW = "bulkOpr" # the bulk operation # +NON_BULK_OPR_KW = "nonBulkOpr" # non bulk operation # +EXEC_CMD_RULE_KW = "execCmdRule" # the rule that invoke execCmd # +EXEC_MY_RULE_KW = "execMyRule" # the rule is invoked by rsExecMyRule # +STREAM_STDOUT_KW = "streamStdout" # the stream stdout for execCmd # +REG_REPL_KW = "regRepl" # register replica # +AGE_KW = "age" # age of the file for itrim # +DRYRUN_KW = "dryrun" # do a dry run # +ACL_COLLECTION_KW = "aclCollection" # the collection from which the ACL should be used # +NO_CHK_COPY_LEN_KW = "noChkCopyLen" # Don't check the len when transfering # +TICKET_KW = "ticket" # for ticket-based-access # +PURGE_CACHE_KW = "purgeCache" # purge the cache copy right after the operation JMC - backport 4537 +EMPTY_BUNDLE_ONLY_KW = "emptyBundleOnly" # delete emptyBundleOnly # # JMC - backport 4552 +GET_RESOURCE_INFO_OP_TYPE_KW = "getResourceInfoOpType" + +# =-=-=-=-=-=-=- +# JMC - backport 4599 +LOCK_TYPE_KW = "lockType" # valid values are READ_LOCK_TYPE, WRITE_LOCK_TYPE and UNLOCK_TYPE # +LOCK_CMD_KW = "lockCmd" # valid values are SET_LOCK_WAIT_CMD, SET_LOCK_CMD and GET_LOCK_CMD # +LOCK_FD_KW = "lockFd" # Lock file desc for unlock # +MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles # +MAX_BUNDLE_SIZE_KW = "maxBunSize" # max size of a tar bundle in Gbs # +NO_STAGING_KW = "noStaging" + +# =-=-=-=-=-=-=- +MAX_SUB_FILE_KW = "maxSubFile" # max number of files for tar file bundles # # JMC - backport 4771 + +# OBJ_PATH_KW already defined # + +# OBJ_PATH_KW already defined # +# COLL_NAME_KW already defined # +FILE_UID_KW = "fileUid" +FILE_OWNER_KW = "fileOwner" +FILE_GID_KW = "fileGid" +FILE_GROUP_KW = "fileGroup" +FILE_MODE_KW = "fileMode" +FILE_CTIME_KW = "fileCtime" +FILE_MTIME_KW = "fileMtime" +FILE_SOURCE_PATH_KW = "fileSourcePath" +EXCLUDE_FILE_KW = "excludeFile" + +# The following are the keyWord definition for the rescCond key/value pair # +# RESC_NAME_KW is defined above # + +RESC_ZONE_KW = "zoneName" +RESC_LOC_KW = "rescLoc" # resc_net in DB # +RESC_TYPE_KW = "rescType" +RESC_CLASS_KW = "rescClass" +RESC_VAULT_PATH_KW = "rescVaultPath" # resc_def_path in DB # +RESC_STATUS_KW = "rescStatus" +GATEWAY_ADDR_KW = "gateWayAddr" +RESC_MAX_OBJ_SIZE_KW = "rescMaxObjSize" +FREE_SPACE_KW = "freeSpace" +FREE_SPACE_TIME_KW = "freeSpaceTime" +FREE_SPACE_TIMESTAMP_KW = "freeSpaceTimeStamp" +RESC_TYPE_INX_KW = "rescTypeInx" +RESC_CLASS_INX_KW = "rescClassInx" +RESC_ID_KW = "rescId" +RESC_COMMENTS_KW = "rescComments" +RESC_CREATE_KW = "rescCreate" +RESC_MODIFY_KW = "rescModify" + +# The following are the keyWord definition for the userCond key/value pair # + +USER_NAME_CLIENT_KW = "userNameClient" +RODS_ZONE_CLIENT_KW = "rodsZoneClient" +HOST_CLIENT_KW = "hostClient" +CLIENT_ADDR_KW = "clientAddr" +USER_TYPE_CLIENT_KW = "userTypeClient" +AUTH_STR_CLIENT_KW = "authStrClient" # user distin name # +USER_AUTH_SCHEME_CLIENT_KW = "userAuthSchemeClient" +USER_INFO_CLIENT_KW = "userInfoClient" +USER_COMMENT_CLIENT_KW = "userCommentClient" +USER_CREATE_CLIENT_KW = "userCreateClient" +USER_MODIFY_CLIENT_KW = "userModifyClient" +USER_NAME_PROXY_KW = "userNameProxy" +RODS_ZONE_PROXY_KW = "rodsZoneProxy" +HOST_PROXY_KW = "hostProxy" +USER_TYPE_PROXY_KW = "userTypeProxy" +AUTH_STR_PROXY_KW = "authStrProxy" # dn # +USER_AUTH_SCHEME_PROXY_KW = "userAuthSchemeProxy" +USER_INFO_PROXY_KW = "userInfoProxy" +USER_COMMENT_PROXY_KW = "userCommentProxy" +USER_CREATE_PROXY_KW = "userCreateProxy" +USER_MODIFY_PROXY_KW = "userModifyProxy" +ACCESS_PERMISSION_KW = "accessPermission" +NO_CHK_FILE_PERM_KW = "noChkFilePerm" + +# The following are the keyWord definition for the collCond key/value pair # + +COLL_NAME_KW = "collName" +COLL_PARENT_NAME_KW = "collParentName" # parent_coll_name in DB # +COLL_OWNER_NAME_KW = "collOwnername" +COLL_OWNER_ZONE_KW = "collOwnerZone" +COLL_MAP_ID_KW = "collMapId" +COLL_INHERITANCE_KW = "collInheritance" +COLL_COMMENTS_KW = "collComments" +COLL_EXPIRY_KW = "collExpiry" +COLL_CREATE_KW = "collCreate" +COLL_MODIFY_KW = "collModify" +COLL_ACCESS_KW = "collAccess" +COLL_ACCESS_INX_KW = "collAccessInx" +COLL_ID_KW = "collId" + +# The following are the keyWord definitions for the keyValPair_t input to chlModRuleExec. +# +RULE_NAME_KW = "ruleName" +RULE_REI_FILE_PATH_KW = "reiFilePath" +RULE_USER_NAME_KW = "userName" +RULE_EXE_ADDRESS_KW = "exeAddress" +RULE_EXE_TIME_KW = "exeTime" +RULE_EXE_FREQUENCY_KW = "exeFrequency" +RULE_PRIORITY_KW = "priority" +RULE_ESTIMATE_EXE_TIME_KW = "estimateExeTime" +RULE_NOTIFICATION_ADDR_KW = "notificationAddr" +RULE_LAST_EXE_TIME_KW = "lastExeTime" +RULE_EXE_STATUS_KW = "exeStatus" + + +EXCLUDE_FILE_KW = "excludeFile" +AGE_KW = "age" # age of the file for itrim # + +# =-=-=-=-=-=-=- +# irods general keywords definitions +RESC_HIER_STR_KW = "resc_hier" +REPLICA_TOKEN_KW = "replicaToken" +DEST_RESC_HIER_STR_KW = "dest_resc_hier" +IN_PDMO_KW = "in_pdmo" +STAGE_OBJ_KW = "stage_object" +SYNC_OBJ_KW = "sync_object" +IN_REPL_KW = "in_repl" + +# =-=-=-=-=-=-=- +# irods tcp keyword definitions +SOCKET_HANDLE_KW = "tcp_socket_handle" + +# =-=-=-=-=-=-=- +# irods ssl keyword definitions +SSL_HOST_KW = "ssl_host" +SSL_SHARED_SECRET_KW = "ssl_shared_secret" +SSL_KEY_SIZE_KW = "ssl_key_size" +SSL_SALT_SIZE_KW = "ssl_salt_size" +SSL_NUM_HASH_ROUNDS_KW = "ssl_num_hash_rounds" +SSL_ALGORITHM_KW = "ssl_algorithm" + +# =-=-=-=-=-=-=- +# irods data_object keyword definitions +PHYSICAL_PATH_KW = "physical_path" +MODE_KW = "mode_kw" +FLAGS_KW = "flags_kw" +# borrowed RESC_HIER_STR_KW + +# =-=-=-=-=-=-=- +# irods file_object keyword definitions +LOGICAL_PATH_KW = "logical_path" +FILE_DESCRIPTOR_KW = "file_descriptor" +L1_DESC_IDX_KW = "l1_desc_idx" +SIZE_KW = "file_size" +REPL_REQUESTED_KW = "repl_requested" +# borrowed IN_PDMO_KW + + +# =-=-=-=-=-=-=- +# irods structured_object keyword definitions +HOST_ADDR_KW = "host_addr" +ZONE_NAME_KW = "zone_name" +PORT_NUM_KW = "port_num" +SUB_FILE_PATH_KW = "sub_file_path" +# borrowed OFFSET_KW +# borrowed DATA_TYPE_KW +# borrowed OPR_TYPE_KW + +# =-=-=-=-=-=-=- +# irods spec coll keyword definitions +SPEC_COLL_CLASS_KW = "spec_coll_class" +SPEC_COLL_TYPE_KW = "spec_coll_type" +SPEC_COLL_OBJ_PATH_KW = "spec_coll_obj_path" +SPEC_COLL_RESOURCE_KW = "spec_coll_resource" +SPEC_COLL_RESC_HIER_KW = "spec_coll_resc_hier" +SPEC_COLL_PHY_PATH_KW = "spec_coll_phy_path" +SPEC_COLL_CACHE_DIR_KW = "spec_coll_cache_dir" +SPEC_COLL_CACHE_DIRTY = "spec_coll_cache_dirty" +SPEC_COLL_REPL_NUM = "spec_coll_repl_num" + + +DISABLE_STRICT_ACL_KW = "disable_strict_acls" diff --git a/fprc/irods/client/low_level/message/__init__.py b/fprc/irods/client/low_level/message/__init__.py new file mode 100644 index 000000000..e811f3f67 --- /dev/null +++ b/fprc/irods/client/low_level/message/__init__.py @@ -0,0 +1,1240 @@ +"""Define objects related to communication with iRODS server API endpoints.""" + +import ast +import json +import logging +import os +import socket +import struct +import sys +import threading +#import xml.etree.ElementTree as ET_xml +from collections import namedtuple +from typing import Optional +from warnings import warn + +#import irods.exception as ex + +from . import quasixml +from .message import Message +from .property_types import ( + ArrayProperty, + BinaryProperty, + IntegerProperty, + LongProperty, + StringProperty, + SubmessageProperty, +) +#from ..api_number import api_number + + +class Bad_AVU_Field(ValueError): + pass + + +_TUPLE_LIKE_TYPES = (tuple, list) + + +def _qxml_server_version(var): + val = os.environ.get(var, "()") + vsn = val and ast.literal_eval(val) + if not isinstance(vsn, _TUPLE_LIKE_TYPES): + return None + return tuple(vsn) + + +import enum + + +class XML_Parser_Type(enum.Enum): + _invalid = 0 + STANDARD_XML = 1 + QUASI_XML = 2 + SECURE_XML = 3 + + +# This creates a mapping from the "valid" (nonzero) XML_Parser_Type enums -- those which represent the actual parser +# choices -- to their corresponding names as strings (e.g. XML_Parser_Type.STANDARD_XML is mapped to 'STANDARD_XML'): +PARSER_TYPE_STRINGS = {v: k for k, v in XML_Parser_Type.__members__.items() if v.value != 0} + +# We maintain values on a per-thread basis of: +# - the server version with which we're communicating +# - which of the choices of parser (STANDARD_XML or QUASI_XML) we're using + +_thrlocal = threading.local() + +# The packStruct message parser defaults to STANDARD_XML but we can override it by setting the +# environment variable PYTHON_IRODSCLIENT_DEFAULT_XML to either 'SECURE_XML' or 'QUASI_XML'. +# If QUASI_XML is selected, the environment variable PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION +# may also be set to a tuple "X,Y,Z" to inform the client of the connected iRODS server version. +# If we set a value for the version, it can be either: +# * 4,2,8 to work with that server version and older ones which incorrectly encoded back-ticks as ''' +# * an empty tuple "()" or something >= 4,2,9 to work with newer servers to allow a flexible character +# set within iRODS protocol. + + +class BadXMLSpec(RuntimeError): + pass + + +_Quasi_Xml_Server_Version = _qxml_server_version("PYTHON_IRODSCLIENT_QUASI_XML_SERVER_VERSION") +if _Quasi_Xml_Server_Version is None: # unspecified in environment yields empty tuple () + raise BadXMLSpec("Must properly specify a server version to use QUASI_XML") + +_XML_strings = {k: v for k, v in vars(XML_Parser_Type).items() if k.endswith("_XML")} + +_default_XML = XML_Parser_Type.QUASI_XML + +def current_XML_parser(get_module=False): + d = getattr(_thrlocal, "xml_type", _default_XML) + return d if not get_module else _get_XML_parser_for(d) + + +def default_XML_parser(get_module=False): + d = _default_XML + return d if not get_module else _get_XML_parser_for(d) + + +def _get_XML_parser_for(d): + return sys.modules[__name__]._XML_parser[d] + + +def string_for_XML_parser(parser_enum): + return PARSER_TYPE_STRINGS[parser_enum] + + +def XML_entities_active(): + Server = getattr(_thrlocal, "irods_server_version", _Quasi_Xml_Server_Version) + return [ + ("&", "&"), # note: order matters. & must be encoded first. + ("<", "<"), + (">", ">"), + ('"', """), + ( + "'" if not (Server) or Server >= (4, 2, 9) else "`", + "'", + ), # https://github.com/irods/irods/issues/4132 + ] + + +# ET() [no-args form] will just fetch the current thread's XML parser settings + + +def ET(xml_type=(), server_version=None): + """ + Return the module used to parse XML from iRODS protocol messages text. + + May also be used to specify the following attributes of the currently running thread: + + `xml_type', if given, should be 1 for STANDARD_XML or 2 for QUASI_XML. + * QUASI_XML is custom parser designed to be more compatible with the use of + non-printable characters in object names. + * STANDARD_XML uses the standard module, xml.etree.ElementTree. + * an empty tuple is the default argument for `xml_type', imparting the same + semantics as for the argumentless form ET(), ie., short-circuit any parser change. + + `server_version', if given, should be a list or tuple specifying the version of the connected iRODS server. + + """ + if xml_type != (): + _thrlocal.xml_type = ( + default_XML_parser() if xml_type in (None, XML_Parser_Type(0)) else XML_Parser_Type(xml_type) + ) + if isinstance(server_version, _TUPLE_LIKE_TYPES): + _thrlocal.irods_server_version = tuple( + server_version + ) # A default server version for Quasi-XML parsing is set (from the environment) and + return _get_XML_parser_for( + current_XML_parser() + ) # applies to all threads in which ET() has not been called to update the value. + + +logger = logging.getLogger(__name__) + +# The symbol _IRODS_VERSION is for internal use in testing only. It indicates the current +# server version for which PRC has maintained compatibility. Attempting the unit tests with +# more recent servers will fail by design. +_IRODS_VERSION = (5, 0, 2, "d") + +# This is the older, now deprecated, version of the above symbol. +_deprecated_names = {"IRODS_VERSION": _IRODS_VERSION} + + +def __getattr__(name): + if name in _deprecated_names: + warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2) + return _deprecated_names[name] + + global __XML_parser + # Define the _XML_parser module variable only when accessed (#802). + if name == '_XML_parser': + impl = globals().get('__XML_parser') + if not impl: + impl = __XML_parser = { +# XML_Parser_Type.STANDARD_XML: ET_xml, + XML_Parser_Type.QUASI_XML: quasixml, + } + return impl + + message = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(message) + + +UNICODE = str + +_METADATA_FIELD_TYPES = {str, UNICODE, bytes} + + +# Necessary for older python (<3.7): + + +def _socket_is_blocking(sk): + try: + return sk.getblocking() + except AttributeError: + # Python 3.7+ docs say sock.getblocking() is equivalent to checking if sock.gettimeout() == 0, but this is misleading. + # Manual testing shows this to be a more accurate equivalent: + timeout = sk.gettimeout() + return timeout is None or timeout > 0 + + +def _recv_message_in_len(sock, size): + size_left = size + retbuf = None + + # Get socket properties for debug and exception messages. + is_blocking = _socket_is_blocking(sock) + timeout = sock.gettimeout() + + logger.debug("is_blocking: %s", is_blocking) + logger.debug("timeout: %s", timeout) + + while size_left > 0: + try: + buf = sock.recv(size_left, socket.MSG_WAITALL) + except (AttributeError, ValueError): + buf = sock.recv(size_left) + except OSError as e: + # skip only Windows error 10045 + if getattr(e, "winerror", 0) != 10045: + raise + buf = sock.recv(size_left) + + # This prevents an infinite loop. If the call to recv() + # returns an empty buffer, break out of the loop. + if len(buf) == 0: + break + size_left -= len(buf) + if retbuf is None: + retbuf = buf + else: + retbuf += buf + + # This method is supposed to read and return 'size' + # bytes from the socket. If it reads no bytes (retbuf + # will be None), or if it reads less number of bytes + # than 'size', throw a socket.error exception + if retbuf is None or len(retbuf) != size: + retbuf_size = len(retbuf) if retbuf is not None else 0 + msg = "Read {} bytes from socket instead of expected {} bytes".format(retbuf_size, size) + raise socket.error(msg) + + return retbuf + + +def _recv_message_into(sock, buffer, size): + size_left = size + index = 0 + mv = memoryview(buffer) + while size_left > 0: + try: + rsize = sock.recv_into(mv[index:], size_left, socket.MSG_WAITALL) + except (AttributeError, ValueError): + rsize = sock.recv_into(mv[index:], size_left) + except OSError as e: + # skip only Windows error 10045 + if getattr(e, "winerror", 0) != 10045: + raise + rsize = sock.recv_into(mv[index:], size_left) + size_left -= rsize + index += rsize + return mv[:index] + + +# ------------------------------------ + + +class BinBytesBuf(Message): + _name = "BinBytesBuf_PI" + buflen = IntegerProperty() + buf = BinaryProperty() + + +class JSON_Binary_Response(BinBytesBuf): + pass + + +class XMLMessageNotConvertibleToJSON(Exception): + pass + + +class iRODSMessage: + class ResponseNotParseable(Exception): + """ + Raised by get_main_message(ResponseClass) to indicate a server response + wraps a msg string that is the `None' object rather than an XML String. + (Not raised for the ResponseClass is irods.message.Error; see source of + get_main_message for further detail.) + """ + + pass + + def __init__(self, msg_type=b"", msg=None, error=b"", bs=b"", int_info=0): + self.msg_type = msg_type + self.msg = msg + self.error = error + self.bs = bs + self.int_info = int_info + + def get_json_encoded_struct(self): + """For messages having STR_PI and *BytesBuf_PI in the highest level XML tag. + + Invoke this method to recover a (usually JSON-formatted) server message + returned by a server API. + """ + Xml = ET().fromstring(self.msg.replace(b"\0", b"")) + + # Handle STR_PI case, which corresponds to server APIs with a 'char**' output parameter. + if Xml.tag == "STR_PI": + STR_PI_element = Xml.find("myStr") + if STR_PI_element is not None: + return json.loads(STR_PI_element.text) + + # Handle remaining cases, i.e. BinBytesBuf_PI and BytesBuf_PI. + json_str = getattr(Xml.find("buf"), "text", None) + if json_str is None: + error_text = "Message does not have a suitable 'buf' tag from which to extract text or binary content." + raise XMLMessageNotConvertibleToJSON(error_text) + + if Xml.tag == "BinBytesBuf_PI": + mybin = JSON_Binary_Response() + mybin.unpack(Xml) + json_str = mybin.buf.replace(b"\0", b"").decode() + + if Xml.tag in ("BinBytesBuf_PI", "BytesBuf_PI"): + return json.loads(json_str) + + error_text = "Inappropriate top-level tag '{Xml.tag}' used in iRODSMessage.get_json_encoded_struct".format( + **locals() + ) + raise XMLMessageNotConvertibleToJSON(error_text) + + @staticmethod + def recv(sock): + # rsp_header_size = sock.recv(4, socket.MSG_WAITALL) + rsp_header_size = _recv_message_in_len(sock, 4) + rsp_header_size = struct.unpack(">i", rsp_header_size)[0] + # rsp_header = sock.recv(rsp_header_size, socket.MSG_WAITALL) + rsp_header = _recv_message_in_len(sock, rsp_header_size) + + xml_root = ET().fromstring(rsp_header) + msg_type = xml_root.find("type").text + msg_len = int(xml_root.find("msgLen").text) + err_len = int(xml_root.find("errorLen").text) + bs_len = int(xml_root.find("bsLen").text) + int_info = int(xml_root.find("intInfo").text) + + # message = sock.recv(msg_len, socket.MSG_WAITALL) if msg_len != 0 else + # None + message = _recv_message_in_len(sock, msg_len) if msg_len != 0 else None + # error = sock.recv(err_len, socket.MSG_WAITALL) if err_len != 0 else + # None + error = _recv_message_in_len(sock, err_len) if err_len != 0 else None + # bs = sock.recv(bs_len, socket.MSG_WAITALL) if bs_len != 0 else None + bs = _recv_message_in_len(sock, bs_len) if bs_len != 0 else None + + # if message: + # logger.debug(message) + + return iRODSMessage(msg_type, message, error, bs, int_info) + + @staticmethod + def recv_into(sock, buffer): + rsp_header_size = _recv_message_in_len(sock, 4) + rsp_header_size = struct.unpack(">i", rsp_header_size)[0] + rsp_header = _recv_message_in_len(sock, rsp_header_size) + + xml_root = ET().fromstring(rsp_header) + msg_type = xml_root.find("type").text + msg_len = int(xml_root.find("msgLen").text) + err_len = int(xml_root.find("errorLen").text) + bs_len = int(xml_root.find("bsLen").text) + int_info = int(xml_root.find("intInfo").text) + + message = _recv_message_in_len(sock, msg_len) if msg_len != 0 else None + error = _recv_message_in_len(sock, err_len) if err_len != 0 else None + bs = _recv_message_into(sock, buffer, bs_len) if bs_len != 0 else None + + return iRODSMessage(msg_type, message, error, bs, int_info) + + @staticmethod + def encode_unicode(my_str): + if isinstance(my_str, UNICODE): + return my_str.encode("utf-8") + else: + return my_str + + @staticmethod + def pack_header(type, msg_len, err_len, bs_len, int_info): + msg_header = ( + "" + "{}" + "{}" + "{}" + "{}" + "{}" + "" + ).format(type, msg_len, err_len, bs_len, int_info) + + # encode if needed + msg_header = iRODSMessage.encode_unicode(msg_header) + + # pack length + msg_header_length = struct.pack(">i", len(msg_header)) + + return msg_header_length + msg_header + + def pack(self): + # pack main message and endcode if needed + if self.msg: + main_msg = self.encode_unicode(self.msg.pack()) + else: + main_msg = b"" + + # encode message parts if needed + self.error = self.encode_unicode(self.error) + self.bs = self.encode_unicode(self.bs) + + # pack header + packed_header = self.pack_header(self.msg_type, len(main_msg), len(self.error), len(self.bs), self.int_info) + + return packed_header + main_msg + self.error + self.bs + + def get_main_message(self, cls, r_error=None): + msg = cls() + logger.debug("Attempt to parse server response [%r] as class [%r].", self.msg, cls) + if self.error and isinstance(r_error, RErrorStack): + r_error.fill(iRODSMessage(msg=self.error).get_main_message(Error)) + if self.msg is None: + if cls is not Error: + # - For dedicated API response classes being built from server response, allow catching + # of the exception. However, let iRODS errors such as CAT_NO_ROWS_FOUND to filter + # through as usual for express reporting by instances of irods.connection.Connection . + message = "Server response was {self.msg} while parsing as [{cls}]".format(**locals()) + raise self.ResponseNotParseable(message) + msg.unpack(ET().fromstring(self.msg)) + return msg + + +# define CS_NEG_PI "int status; str result[MAX_NAME_LEN];" +class ClientServerNegotiation(Message): + _name = "CS_NEG_PI" + status = IntegerProperty() + result = StringProperty() + + +# define StartupPack_PI "int irodsProt; int reconnFlag; int connectCnt; +# str proxyUser[NAME_LEN]; str proxyRcatZone[NAME_LEN]; str +# clientUser[NAME_LEN]; str clientRcatZone[NAME_LEN]; str +# relVersion[NAME_LEN]; str apiVersion[NAME_LEN]; str option[NAME_LEN];" + + +class StartupPack(Message): + _name = "StartupPack_PI" + + def __init__(self, proxy_user, client_user, application_name=""): + super(StartupPack, self).__init__() + if proxy_user and client_user: + self.irodsProt = 1 + self.connectCnt = 0 + self.proxyUser, self.proxyRcatZone = proxy_user + self.clientUser, self.clientRcatZone = client_user + self.relVersion = "rods{}.{}.{}".format(*_IRODS_VERSION) + self.apiVersion = "{3}".format(*_IRODS_VERSION) + self.option = application_name + + irodsProt = IntegerProperty() + reconnFlag = IntegerProperty() + connectCnt = IntegerProperty() + proxyUser = StringProperty() + proxyRcatZone = StringProperty() + clientUser = StringProperty() + clientRcatZone = StringProperty() + relVersion = StringProperty() + apiVersion = StringProperty() + option = StringProperty() + + +# define authResponseInp_PI "bin *response(RESPONSE_LEN); str *username;" + + +class AuthResponse(Message): + _name = "authResponseInp_PI" + response = BinaryProperty(16) + username = StringProperty() + + +class AuthChallenge(Message): + _name = "authRequestOut_PI" + challenge = BinaryProperty(64) + + +class AuthPluginOut(Message): + _name = "authPlugReqOut_PI" + result_ = StringProperty() + # result_ = BinaryProperty(16) + + +# The following PamAuthRequest* classes correspond to older, less generic +# PAM auth api in iRODS, but one which allowed longer password tokens. +# They are contributed by Rick van de Hoef at Utrecht Univ, c. June 2021: + + +class PamAuthRequest(Message): + _name = "pamAuthRequestInp_PI" + pamUser = StringProperty() + pamPassword = StringProperty() + timeToLive = IntegerProperty() + + +class PamAuthRequestOut(Message): + _name = "pamAuthRequestOut_PI" + irodsPamPassword = StringProperty() + + @property + def result_(self): + return self.irodsPamPassword + + +# define InxIvalPair_PI "int iiLen; int *inx(iiLen); int *ivalue(iiLen);" + + +class JSON_Binary_Request(BinBytesBuf): + """A message body whose payload is BinBytesBuf containing JSON.""" + + def __init__(self, msg_struct): + """Initialize with a Python data structure that will be converted to JSON.""" + super(JSON_Binary_Request, self).__init__() + string = json.dumps(msg_struct) + self.buf = string + self.buflen = len(string) + + +class BytesBuf(Message): + """A generic structure carrying text content""" + + _name = "BytesBuf_PI" + buflen = IntegerProperty() + buf = StringProperty() + + def __init__(self, string, *v, **kw): + super(BytesBuf, self).__init__(*v, **kw) + self.buf = string + self.buflen = len(self.buf) + + +class JSON_XMLFramed_Request(BytesBuf): + """A message body whose payload is a BytesBuf containing JSON.""" + + def __init__(self, msg_struct): + """Initialize with a Python data structure that will be converted to JSON.""" + s = json.dumps(msg_struct) + super(JSON_XMLFramed_Request, self).__init__(s) + + +def JSON_Message(msg_struct, server_version=()): + cls = JSON_XMLFramed_Request if server_version < (4, 2, 9) else JSON_Binary_Request + return cls(msg_struct) + + +class PluginAuthMessage(Message): + _name = "authPlugReqInp_PI" + auth_scheme_ = StringProperty() + context_ = StringProperty() + + +class _OrderedMultiMapping: + def keys(self): + return self._keys + + def values(self): + return self._values + + def __len__(self): + return len(self._keys) + + def __init__(self, list_of_keyval_tuples): + self._keys = [] + self._values = [] + for k, v in list_of_keyval_tuples: + self._keys.append(k) + self._values.append(v) + + +class IntegerIntegerMap(Message): + _name = "InxIvalPair_PI" + + def __init__(self, data=None): + super(IntegerIntegerMap, self).__init__() + self.iiLen = 0 + if data: + self.iiLen = len(data) + self.inx = data.keys() + self.ivalue = data.values() + + iiLen = IntegerProperty() + inx = ArrayProperty(IntegerProperty()) + ivalue = ArrayProperty(IntegerProperty()) + + +# define InxValPair_PI "int isLen; int *inx(isLen); str *svalue[isLen];" + + +class IntegerStringMap(Message): + _name = "InxValPair_PI" + + def __init__(self, data=None): + super(IntegerStringMap, self).__init__() + self.isLen = 0 + if data: + self.isLen = len(data) + self.inx = data.keys() + self.svalue = data.values() + + isLen = IntegerProperty() + inx = ArrayProperty(IntegerProperty()) + svalue = ArrayProperty(StringProperty()) + + +# define KeyValPair_PI "int ssLen; str *keyWord[ssLen]; str *svalue[ssLen];" + + +class StringStringMap(Message): + _name = "KeyValPair_PI" + + def __init__(self, data=None): + super(StringStringMap, self).__init__() + self.ssLen = 0 + if data: + self.ssLen = len(data) + self.keyWord = data.keys() + self.svalue = data.values() + + ssLen = IntegerProperty() + keyWord = ArrayProperty(StringProperty()) + svalue = ArrayProperty(StringProperty()) + + +# define GenQueryInp_PI "int maxRows; int continueInx; int +# partialStartIndex; int options; struct KeyValPair_PI; struct +# InxIvalPair_PI; struct InxValPair_PI;" + + +class GenQueryRequest(Message): + _name = "GenQueryInp_PI" + maxRows = IntegerProperty() + continueInx = IntegerProperty() + partialStartIndex = IntegerProperty() + options = IntegerProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + InxIvalPair_PI = SubmessageProperty(IntegerIntegerMap) + InxValPair_PI = SubmessageProperty(IntegerStringMap) + + +# define SqlResult_PI "int attriInx; int reslen; str *value(rowCnt)(reslen);" + + +class GenQueryResponseColumn(Message): + _name = "SqlResult_PI" + attriInx = IntegerProperty() + reslen = IntegerProperty() + value = ArrayProperty(StringProperty()) + + +# define GenQueryOut_PI "int rowCnt; int attriCnt; int continueInx; int +# totalRowCount; struct SqlResult_PI[MAX_SQL_ATTR];" + + +class GenQueryResponse(Message): + _name = "GenQueryOut_PI" + rowCnt = IntegerProperty() + attriCnt = IntegerProperty() + continueInx = IntegerProperty() + totalRowCount = IntegerProperty() + SqlResult_PI = ArrayProperty(SubmessageProperty(GenQueryResponseColumn)) + + +# define DataObjInp_PI "str objPath[MAX_NAME_LEN]; int createMode; int +# openFlags; double offset; double dataSize; int numThreads; int oprType; +# struct *SpecColl_PI; struct KeyValPair_PI;" + + +class GenQuery2Request(Message): + _name = "Genquery2Input_PI" + query_string = StringProperty() + zone = StringProperty() + sql_only = IntegerProperty() + column_mappings = IntegerProperty() + + +class FileOpenRequest(Message): + _name = "DataObjInp_PI" + objPath = StringProperty() + createMode = IntegerProperty() + openFlags = IntegerProperty() + offset = LongProperty() + dataSize = LongProperty() + numThreads = IntegerProperty() + oprType = IntegerProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +class DataObjChksumRequest(FileOpenRequest): + """Report and/or generate a data object's checksum.""" + + def __init__(self, path, **chksumOptions): + """Construct the request using the path of a data object.""" + super(DataObjChksumRequest, self).__init__() + for attr, prop in vars(FileOpenRequest).items(): + if isinstance(prop, (IntegerProperty, LongProperty)): + setattr(self, attr, 0) + self.objPath = path + self.KeyValPair_PI = StringStringMap(chksumOptions) + + +class DataObjChksumResponse(Message): + name = "Str_PI" + myStr = StringProperty() + + +# define OpenedDataObjInp_PI "int l1descInx; int len; int whence; int +# oprType; double offset; double bytesWritten; struct KeyValPair_PI;" + + +class OpenedDataObjRequest(Message): + _name = "OpenedDataObjInp_PI" + l1descInx = IntegerProperty() + len = IntegerProperty() + whence = IntegerProperty() + oprType = IntegerProperty() + offset = LongProperty() + bytesWritten = LongProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +# define fileLseekOut_PI "double offset;" + + +class FileSeekResponse(Message): + _name = "fileLseekOut_PI" + offset = LongProperty() + + +# define DataObjCopyInp_PI "struct DataObjInp_PI; struct DataObjInp_PI;" + + +class ObjCopyRequest(Message): + _name = "DataObjCopyInp_PI" + srcDataObjInp_PI = SubmessageProperty(FileOpenRequest) + destDataObjInp_PI = SubmessageProperty(FileOpenRequest) + + +# define ModAVUMetadataInp_PI "str *arg0; str *arg1; str *arg2; str *arg3; +# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9; struct KeyValPair_PI" + + +class MetadataRequest(Message): + _name = "ModAVUMetadataInp_PI" + + def __init__(self, *args, **metadata_opts): + super(MetadataRequest, self).__init__() + + # Python3 does not have types.NoneType + NoneType = type(None) + + def field_name(i): + return ("attribute", "value", "unit")[i - 3] + + # We now enforce these requirements of the scalars (ie. AVU fields) submitted to the metadata call: + # * All fields must each be of type str, except that the units field can be the None object. + # * Attribute and Value must be of type str (a Python string) as well as nonzero length. + for i, arg in enumerate(args): + error = None + + # Raise usage error if any of the AVU fields (args 3 to 5 inclusive) do not meet the above constraints. + if i in (3, 4, 5): + if type(arg) not in ( + _METADATA_FIELD_TYPES + if i < 5 + else { + NoneType, + } + | _METADATA_FIELD_TYPES + ): + error = Bad_AVU_Field( + "AVU %s (%r) has incorrect type. AVU fields must be strings, except for units, which could be None." + % (field_name(i), arg) + ) + elif i < 5 and not (arg): + error = Bad_AVU_Field("AVU %s (%r) is zero-length." % (field_name(i), arg)) + if error is not None: + raise error + + # If there is no error, set the attribute in the request message. + if arg not in {None, b"", ""}: + setattr(self, "arg%d" % i, arg) + + self.KeyValPair_PI = StringStringMap(metadata_opts) + + arg0 = StringProperty() + arg1 = StringProperty() + arg2 = StringProperty() + arg3 = StringProperty() + arg4 = StringProperty() + arg5 = StringProperty() + arg6 = StringProperty() + arg7 = StringProperty() + arg8 = StringProperty() + arg9 = StringProperty() + + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +# define modAccessControlInp_PI "int recursiveFlag; str *accessLevel; str +# *userName; str *zone; str *path;" + + +class ModAclRequest(Message): + _name = "modAccessControlInp_PI" + recursiveFlag = IntegerProperty() + accessLevel = StringProperty() + userName = StringProperty() + zone = StringProperty() + path = StringProperty() + + +# define CollInp_PI "str collName[MAX_NAME_LEN]; struct KeyValPair_PI;" + + +class CollectionRequest(Message): + _name = "CollInpNew_PI" + collName = StringProperty() + flags = IntegerProperty() + oprType = IntegerProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +# define Version_PI "int status; str relVersion[NAME_LEN]; str +# apiVersion[NAME_LEN]; int reconnPort; str reconnAddr[LONG_NAME_LEN]; int +# cookie;" + + +class VersionResponse(Message): + _name = "Version_PI" + status = IntegerProperty() + relVersion = StringProperty() + apiVersion = StringProperty() + reconnPort = IntegerProperty() + reconnAddr = StringProperty() + cookie = IntegerProperty() + + +class _admin_request_base(Message): + _name: Optional[str] = None + + def __init__(self, *args): + if self.__class__._name is None: + raise NotImplementedError + super(_admin_request_base, self).__init__() + for i in range(10): + if i < len(args) and args[i]: + setattr(self, "arg{0}".format(i), args[i]) + else: + setattr(self, "arg{0}".format(i), "") + + arg0 = StringProperty() + arg1 = StringProperty() + arg2 = StringProperty() + arg3 = StringProperty() + arg4 = StringProperty() + arg5 = StringProperty() + arg6 = StringProperty() + arg7 = StringProperty() + arg8 = StringProperty() + arg9 = StringProperty() + + +# define generalAdminInp_PI "str *arg0; str *arg1; str *arg2; str *arg3; +# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9;" + + +class GeneralAdminRequest(_admin_request_base): + _name = "generalAdminInp_PI" + + +## DWM - try not to rely on irods.api_number (move this function to another module) +#def _do_GeneralAdminRequest(_session_or_accessor, *args): +# sess = _session_or_accessor +# if callable(sess): +# sess = sess() +# message_body = GeneralAdminRequest(*args) +# request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) +# with sess.pool.get_connection() as conn: +# conn.send(request) +# response = conn.recv() +# logger.debug(response.int_info) +# + +# define userAdminInp_PI "str *arg0; str *arg1; str *arg2; str *arg3; +# str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9;" + + +class UserAdminRequest(_admin_request_base): + _name = "userAdminInp_PI" + + +class GetTempPasswordForOtherRequest(Message): + _name = "getTempPasswordForOtherInp_PI" + targetUser = StringProperty() + unused = StringProperty() + + +class GetTempPasswordForOtherOut(Message): + _name = "getTempPasswordForOtherOut_PI" + stringToHashWith = StringProperty() + + +class GetTempPasswordOut(Message): + _name = "getTempPasswordOut_PI" + stringToHashWith = StringProperty() + + +# in iRODS <= 4.2.10: +# define ticketAdminInp_PI "str *arg1; str *arg2; str *arg3; str *arg4; str *arg5; str *arg6;" + +# in iRODS >= 4.2.11: +# define ticketAdminInp_PI "str *arg1; str *arg2; str *arg3; str *arg4; str *arg5; str *arg6; struct KeyValPair_PI;" + + +class TicketAdminRequest(Message): + _name = "ticketAdminInp_PI" + + def __init__(self, *args, **ticketOpts): + super(TicketAdminRequest, self).__init__() + for i in range(6): + if i < len(args) and args[i]: + setattr(self, "arg{0}".format(i + 1), str(args[i])) + else: + setattr(self, "arg{0}".format(i + 1), "") + self.KeyValPair_PI = StringStringMap(ticketOpts) + + arg1 = StringProperty() + arg2 = StringProperty() + arg3 = StringProperty() + arg4 = StringProperty() + arg5 = StringProperty() + arg6 = StringProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +# define specificQueryInp_PI "str *sql; str *arg1; str *arg2; str *arg3; str *arg4; str *arg5; str *arg6; str *arg7; str *arg8; str *arg9; str *arg10; int maxRows; int continueInx; int rowOffset; int options; struct KeyValPair_PI;" + + +class SpecificQueryRequest(Message): + _name = "specificQueryInp_PI" + sql = StringProperty() + + arg1 = StringProperty() + arg2 = StringProperty() + arg3 = StringProperty() + arg4 = StringProperty() + arg5 = StringProperty() + arg6 = StringProperty() + arg7 = StringProperty() + arg8 = StringProperty() + arg9 = StringProperty() + arg10 = StringProperty() + + maxRows = IntegerProperty() + continueInx = IntegerProperty() + rowOffset = IntegerProperty() + options = IntegerProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + + +# define RHostAddr_PI "str hostAddr[LONG_NAME_LEN]; str +# rodsZone[NAME_LEN]; int port; int dummyInt;" + + +class RodsHostAddress(Message): + _name = "RHostAddr_PI" + hostAddr = StringProperty() + rodsZone = StringProperty() + port = IntegerProperty() + dummyInt = IntegerProperty() + + +# define MsParam_PI "str *label; piStr *type; ?type *inOutStruct; struct +# *BinBytesBuf_PI;" + + +class MsParam(Message): + _name = "MsParam_PI" + label = StringProperty() + type = StringProperty() + + # for packing + inOutStruct = SubmessageProperty() + BinBytesBuf_PI = SubmessageProperty(BinBytesBuf) + + # override Message.unpack() to unpack inOutStruct + # depending on the received element + def unpack(self, root): + for name, prop in self._ordered_properties: + if name == "inOutStruct": + continue + + unpacked_value = prop.unpack(root.findall(name)) + self._values[name] = unpacked_value + + # type tells us what type of data structure we are unpacking + # e.g: ExecCmdOut_PI + if name == "type": + # unpack struct accordingly + message_class = globals()[unpacked_value] + self._values["inOutStruct"] = SubmessageProperty(message_class).unpack(root.findall(unpacked_value)) + + +# define MsParamArray_PI "int paramLen; int oprType; struct +# *MsParam_PI[paramLen];" + + +class MsParamArray(Message): + _name = "MsParamArray_PI" + paramLen = IntegerProperty() + oprType = IntegerProperty() + MsParam_PI = ArrayProperty(SubmessageProperty(MsParam)) + + +# define ExecMyRuleInp_PI "str myRule[META_STR_LEN]; struct RHostAddr_PI; +# struct KeyValPair_PI; str outParamDesc[LONG_NAME_LEN]; struct +# *MsParamArray_PI;" + + +class RuleExecutionRequest(Message): + _name = "ExecMyRuleInp_PI" + myRule = StringProperty() + addr = SubmessageProperty(RodsHostAddress) + condInput = SubmessageProperty(StringStringMap) + outParamDesc = StringProperty() + inpParamArray = SubmessageProperty(MsParamArray) + + +# define ExecCmdOut_PI "struct BinBytesBuf_PI; struct BinBytesBuf_PI; int +# status;" + + +class ExecCmdOut_PI(Message): + """ + In this case the above class name must match the name + of its root element to be unpacked dynamically, + since it is one of the possible types for MsParam. + """ + + _name = "ExecCmdOut_PI" + + # for packing + stdoutBuf = SubmessageProperty(BinBytesBuf) + stderrBuf = SubmessageProperty(BinBytesBuf) + + status = IntegerProperty() + + # need custom unpacking since both buffers have the same element name + def unpack(self, root): + for name, prop in self._ordered_properties: + if name == "stdoutBuf": + unpacked_value = prop.unpack(root.findall(prop.message_cls._name)[:1]) + + elif name == "stderrBuf": + unpacked_value = prop.unpack(root.findall(prop.message_cls._name)[1:]) + + else: + unpacked_value = prop.unpack(root.findall(name)) + + self._values[name] = unpacked_value + + +# define STR_PI "str myStr;" + + +class STR_PI(Message): + """ + Another "returnable" MsParam type + """ + + _name = "STR_PI" + myStr = StringProperty() + + +def DataObjInfo_for_session(session): + + class DataObjInfo(Message): + _name = "DataObjInfo_PI" + objPath = StringProperty() + rescName = StringProperty() + rescHier = StringProperty() + dataType = StringProperty() + dataSize = LongProperty() + chksum = StringProperty() + version = StringProperty() + filePath = StringProperty() + dataOwnerName = StringProperty() + dataOwnerZone = StringProperty() + replNum = IntegerProperty() + replStatus = IntegerProperty() + statusString = StringProperty() + dataId = LongProperty() + collId = LongProperty() + dataMapId = IntegerProperty() + dataComments = StringProperty() + dataMode = StringProperty() + dataExpiry = StringProperty() + dataCreate = StringProperty() + dataModify = StringProperty() + dataAccess = StringProperty() + dataAccessInx = IntegerProperty() + writeFlag = IntegerProperty() + destRescName = StringProperty() + backupRescName = StringProperty() + subPath = StringProperty() + specColl = IntegerProperty() + regUid = IntegerProperty() + otherFlags = IntegerProperty() + KeyValPair_PI = SubmessageProperty(StringStringMap) + in_pdmo = StringProperty() + next = IntegerProperty() + rescId = LongProperty() + + class _DataObjInfo_for_iRODS_5(DataObjInfo): + dataAccessTime = StringProperty() + + return DataObjInfo if session.server_version < (5,) else _DataObjInfo_for_iRODS_5 + + +def ModDataObjMeta_for_session(session): + + doi_class = DataObjInfo_for_session(session) + + class ModDataObjMeta(Message): + _name = "ModDataObjMeta_PI" + dataObjInfo = SubmessageProperty(doi_class) + regParam = SubmessageProperty(StringStringMap) + + return ModDataObjMeta + + +# -- A tuple-descended class which facilitates filling in a +# quasi-RError stack from a JSON formatted list. + +_Server_Status_Message = namedtuple("_Server_Status_Message", ("msg", "status")) + + +class RErrorStack(list): + """A list of returned RErrors.""" + + def __init__(self, Err=None): + """Initialize from the `errors' member of an API return message.""" + super(RErrorStack, self).__init__() # 'list' class initialization + self.fill(Err) + + def fill(self, Err=None): + + # first, we try to parse from a JSON list, as this is how message and status return the Data.chksum call. + if isinstance(Err, (tuple, list)): + self[:] = [RError(_Server_Status_Message(msg=elem["message"], status=elem["error_code"])) for elem in Err] + return + + # next, we try to parse from a a response message - eg. as returned by the Rule.execute API call when a rule fails. + if Err is not None: + self[:] = [RError(Err.RErrMsg_PI[i]) for i in range(Err.count)] + + +class RError: + """One of a list of RError messages potentially returned to the client + from an iRODS API call.""" + + Encoding = "utf-8" + + def __init__(self, entry): + """Initialize from one member of the RErrMsg_PI array.""" + super(RError, self).__init__() + self.raw_msg_ = entry.msg + self.status_ = entry.status + + @property + def message(self): # return self.raw_msg_.decode(self.Encoding) + msg_ = self.raw_msg_ + if type(msg_) is UNICODE: + return msg_ + elif type(msg_) is bytes: + return msg_.decode(self.Encoding) + else: + raise RuntimeError("bad msg type in", msg_) + + @property + def status(self): + return int(self.status_) + +## DWM - do we need to rely on irods.exception? relocate to free func +# @property +# def status_str(self): +# """Retrieve the IRODS error identifier.""" +# return ex.get_exception_class_by_code(self.status, name_only=True) + + def __str__(self): + """Retrieve the error message text.""" + return self.message + + def __int__(self): + """Retrieve integer error code.""" + return self.status + + def __repr__(self): + """Show both the message and iRODS error type (both integer and human-readable).""" + return f"{self.__class__.__name__}" \ + f"" #f" {self.status_str}>" + + +# define RErrMsg_PI "int status; str msg[ERR_MSG_LEN];" + + +class ErrorMessage(Message): + _name = "RErrMsg_PI" + status = IntegerProperty() + msg = StringProperty() + + +# define RError_PI "int count; struct *RErrMsg_PI[count];" + + +class Error(Message): + _name = "RError_PI" + count = IntegerProperty() + RErrMsg_PI = ArrayProperty(SubmessageProperty(ErrorMessage)) + + +def empty_gen_query_out(cols): + sql_results = [GenQueryResponseColumn(attriInx=col.icat_id, value=[]) for col in cols] + gqo = GenQueryResponse(rowCnt=0, attriCnt=len(cols), SqlResult_PI=sql_results) + return gqo diff --git a/fprc/irods/client/low_level/message/message.py b/fprc/irods/client/low_level/message/message.py new file mode 100644 index 000000000..f712579eb --- /dev/null +++ b/fprc/irods/client/low_level/message/message.py @@ -0,0 +1,31 @@ +# http://askawizard.blogspot.com/2008/10/ordered-properties-python-saga-part-5.html +from .ordered import OrderedMetaclass, OrderedClass + + +class MessageMetaclass(OrderedMetaclass): + def __init__(self, name, bases, attys): + super(MessageMetaclass, self).__init__(name, bases, attys) + for name, prop in self._ordered_properties: + prop.dub(name) + + +class Message(OrderedClass, metaclass=MessageMetaclass): + def __init__(self, *args, **kwargs): + super(Message, self).__init__() + self._values = {} + for name, _ in self._ordered_properties: + if name in kwargs: + self._values[name] = kwargs[name] + + def pack(self): + values = [] + values.append("<%s>" % self.__class__._name) + for name, prop in self._ordered_properties: + if name in self._values: + values.append(prop.pack(self._values[name])) + values.append("" % self.__class__._name) + return "".join(values) + + def unpack(self, root): + for name, prop in self._ordered_properties: + self._values[name] = prop.unpack(root.findall(name)) diff --git a/fprc/irods/client/low_level/message/ordered.py b/fprc/irods/client/low_level/message/ordered.py new file mode 100644 index 000000000..190914407 --- /dev/null +++ b/fprc/irods/client/low_level/message/ordered.py @@ -0,0 +1,30 @@ +# Ordered property classes stolen from Kris Kowal of Ask a Wizard +# http://askawizard.blogspot.com/2008/10/ordered-properties-python-saga-part-5.html +from itertools import count + +next_counter = count().__next__ + + +class OrderedProperty: + def __init__(self, *args, **kws): + self._creation_counter = next_counter() + super(OrderedProperty, self).__init__(*args, **kws) + + +class OrderedMetaclass(type): + def __init__(self, name, bases, attys): + super(OrderedMetaclass, self).__init__(name, bases, attys) + self._creation_counter = next_counter() + self._ordered_properties = sorted( + ( + (name, value) + for base in reversed(self.__mro__) + for name, value in base.__dict__.items() + if isinstance(value, OrderedProperty) or isinstance(value, OrderedMetaclass) + ), + key=lambda property: property[1]._creation_counter, + ) + + +class OrderedClass(metaclass=OrderedMetaclass): + pass diff --git a/fprc/irods/client/low_level/message/property_types.py b/fprc/irods/client/low_level/message/property_types.py new file mode 100644 index 000000000..b3f39750a --- /dev/null +++ b/fprc/irods/client/low_level/message/property_types.py @@ -0,0 +1,116 @@ +from base64 import b64encode, b64decode + +from .ordered import OrderedProperty +from html import escape + + +class MessageProperty(OrderedProperty): + def __get__(self, obj, cls): + return obj._values[self.name] + + def __set__(self, obj, value): + obj._values[self.name] = value + + def dub(self, name): + self.name = name + return self + + def pack(self, value): + values = [] + values.append("<%s>" % self.name) + my_value = self.format(value) + if isinstance(my_value, bytes): + my_value = my_value.decode("utf-8") + values.append(my_value) + values.append("" % self.name) + return "".join(values) + + def unpack(self, els): + if len(els): + el = els[0] + return self.parse(el.text) + return None + + +class IntegerProperty(MessageProperty): + def format(self, value): + return str(value) + + def parse(self, value): + return int(value) + + +class LongProperty(MessageProperty): + def format(self, value): + return str(value) + + def parse(self, value): + return int(value) + + +class BinaryProperty(MessageProperty): + def __init__(self, length=None): + self.length = length + super(BinaryProperty, self).__init__() + + def format(self, value): + if isinstance(value, bytes): + return b64encode(value) + else: + return b64encode(value.encode()) + + def parse(self, value): + val = b64decode(value) + return val + + +class StringProperty(MessageProperty): + def __init__(self, length=None): + self.length = length + super(StringProperty, self).__init__() + + @staticmethod + def escape_xml_string(string): + return escape(string, quote=False) + + def format(self, value): + if isinstance(value, str): + return self.escape_xml_string(value) + + if isinstance(value, bytes): + return self.escape_xml_string(value.decode()) + + return self.escape_xml_string(str(value)) + + def parse(self, value): + return value + + +class ArrayProperty(MessageProperty): + def __init__(self, prop): + self.prop = prop + super(ArrayProperty, self).__init__() + + def pack(self, values): + self.prop.dub(self.name) + return "".join([self.prop.pack(v) for v in values]) + + def unpack(self, els): + return [self.prop.unpack([el]) for el in els] + + +class SubmessageProperty(MessageProperty): + def __init__(self, message_cls=None): + self.message_cls = message_cls + super(SubmessageProperty, self).__init__() + + def pack(self, value): + return value.pack() + + def unpack(self, els): + if len(els): + el = els[0] + msg = self.message_cls() + msg.unpack(el) + return msg + return None diff --git a/fprc/irods/client/low_level/message/quasixml.py b/fprc/irods/client/low_level/message/quasixml.py new file mode 100644 index 000000000..4e449fb6b --- /dev/null +++ b/fprc/irods/client/low_level/message/quasixml.py @@ -0,0 +1,193 @@ +# A parser for the iRODS XML-like protocol. +# The interface aims to be compatible with xml.etree.ElementTree, +# at least for the features used by python-irodsclient. + + +class Element: + """ + Represents body. + + (Where `body' is either a string or a list of sub-elements.) + """ + + @property + def tag(self): + return self.name + + def __init__(self, name, body): + """Initialize with the tag's name and the body (i.e. content).""" + if body == []: + # Empty element. + self.text = None + elif type(body) is not list: + # String element: decode body. + body = decode_entities(body) + self.text = body + + self.name = name + self.body = body + + def find(self, name): + """Get first matching child element by name.""" + for x in self.findall(name): + return x + + def findall(self, name): + """Get matching child elements by name.""" + return list(self.findall_(name)) + + def findall_(self, name): + """Get matching child elements by name (generator variant).""" + return (el for el in self.body if el.name == name) + + # For debugging convenience: + def __str__(self): + if type(self.body) is list: + return "<{}>{}".format(self.name, "".join(map(str, self.body)), self.name) + else: + return "<{}>{}".format(self.name, encode_entities(self.body), self.name) + + def __repr__(self): + return "{}({})".format(self.name, repr(self.body)) + + +class Token: + """A utility class for parsing XML.""" + + def __init__(self, s): + """Create a `Token' object from `s', the text comprising the parsed token.""" + self.text = s + + def __repr__(self): + return str(type(self).__name__) + "(" + self.text.decode("utf-8") + ")" + + def __str__(self): + return repr(self) + + +class TokenTagOpen(Token): + """An opening tag ()""" + + +class TokenTagClose(Token): + """An closing tag ()""" + + +class TokenCData(Token): + """Textual element body""" + + +class QuasiXmlParseError(Exception): + """Indicates parse failure of XML protocol data.""" + + +def tokenize(s): + """Parse an XML-ish string into a list of tokens.""" + tokens = [] + + # Consume input until empty. + while True: + nextclose = s.find(b"", 1) + except Exception: + raise QuasiXmlParseError("protocol error: unterminated close tag") + tokens.append(TokenTagClose(name)) + s = s.lstrip() # consume space after closing tag + + # Opening tag? + elif s.startswith(b"<"): + try: + name, s = s[1:].split(b">", 1) + except Exception: + raise QuasiXmlParseError("protocol error: unterminated open tag") + tokens.append(TokenTagOpen(name)) + + else: + # capture cdata till next tag. + try: + cdata, s = s.split(b"<", 1) + except Exception: + raise QuasiXmlParseError("protocol error: unterminated cdata") + s = b"<" + s + tokens.append(TokenCData(cdata)) + + +def fromtokens(tokens): + """Parse XML-ish tokens into an Element.""" + + def parse_elem(tokens): + """Parse some tokens into one Element, and return unconsumed tokens.""" + topen, tokens = tokens[0], tokens[1:] + if type(topen) is not TokenTagOpen: + raise QuasiXmlParseError("protocol error: data does not start with open tag") + + children = [] + cdata = None + + while len(tokens) > 0: + t, tokens = tokens[0], tokens[1:] + if type(t) is TokenTagOpen: + # Slurp a sub-element. + el, tokens = parse_elem([t] + tokens) + children.append(el) + # Continue with non-consumed tokens. + elif type(t) == TokenTagClose: + if t.text != topen.text: + raise QuasiXmlParseError( + "protocol error: close tag <{}> does not match opening tag <{}>".format(t.text, topen.text) + ) + elif cdata is not None and len(children): + raise QuasiXmlParseError("protocol error: mixed cdata and child elements") + return ( + Element( + topen.text.decode("utf-8"), + cdata.decode("utf-8") if cdata is not None else children, + ), + tokens, + ) + else: + cdata = t.text + + elem, rest = parse_elem(tokens) + if rest != []: + raise QuasiXmlParseError("protocol error: trailing data") + + return elem + + +def fromstring(s): + if type(s) is str: + s = s.encode("utf-8") + if type(s) is not bytes: + raise TypeError("expected a bytes-object, got {}".format(type(s).__name__)) + + return fromtokens(tokenize(s)) + + +def encode_entities(s): + from . import XML_entities_active + + for k, v in XML_entities_active(): + s = s.replace(k, v) + return s + + +def decode_entities(s): + from . import XML_entities_active + + rev = list(XML_entities_active()) + rev.reverse() # (make sure & is decoded last) + for k, v in rev: + s = s.replace(v, k) + return s diff --git a/fprc/irods/client/metadata.py b/fprc/irods/client/metadata.py new file mode 100644 index 000000000..8913ba5e2 --- /dev/null +++ b/fprc/irods/client/metadata.py @@ -0,0 +1,49 @@ + +from enum import Enum +from ..api_number import api_number +from .low_level.message import MetadataRequest, iRODSMessage + +class irods_object_type(Enum): + DATA_OBJECT = '-d' + RESOURCE = '-R' + USER = '-u' + COLLECTION = '-C' + +# op can be : "set", "add", "rm" + +def avu_operation( + conn, + object_type: irods_object_type, + op, /, + path, + avu, + **opt): + """ + Applies an AVU operation with a direct calling to the iRODS Metadata API. + + Args: + conn: The Connection object. + object_type: + One of DATA_OBJECT, RESOURCE, USER, or COLLECTION, whichever describes the target object for the 'avu'. + op: One of "add", "set", or "rm" + path: The name or logical path of the target object for the 'avu'. + avu: A sequence containing (name, value[, units]). All sequence members must be strings. + **opt: options to be applied in the metadata API call. + + Returns: + None + + Raises: + An iRODSException corresponding to the particular server error, in case of the API returning a nonzero status code. + """ + + message_body = MetadataRequest( + op, object_type.value, path, *avu, **opt + ) + + request = iRODSMessage( + "RODS_API_REQ", + msg=message_body, int_info=api_number["MOD_AVU_METADATA_AN"] + ) + conn.send(request) + response = conn.recv() diff --git a/fprc/irods/client/pytests/test_data.py b/fprc/irods/client/pytests/test_data.py new file mode 100644 index 000000000..e1df1346b --- /dev/null +++ b/fprc/irods/client/pytests/test_data.py @@ -0,0 +1,56 @@ +from os.path import abspath, dirname, join +import sys +import uuid + +from irods.client.data import ( + data_open, + data_close, + data_unlink, + O_CREAT, + O_WRONLY +) +from irods.client.connection import Connection +from irods.client.metadata import avu_operation, irods_object_type +from irods.client.account import iRODSAccount +from irods.client.query import Query +from irods.client.query.models import DataObjectMeta, DataObject + +def pseudorandom_string(): + return uuid.uuid1().hex + + +account = iRODSAccount( + 'localhost', + 1247, + user := 'rods', + zone := 'tempZone', + password='rods' +) + +PATH = f'/{zone}/home/{user}/abc.dat-' + pseudorandom_string() + +conn = Connection( account ) + +def test_data_and_metadata_create(): + desc = None + try: + # Create a data object. + desc = data_open(conn, PATH, O_WRONLY|O_CREAT) + data_close(conn, desc) + + my_avu = (pseudorandom_string(), 'my-value', 'my-units') + + def my_avu_occurrences(): + "For tests, return the number of avu's both attached to a data object and matching the name field for my_avu" + return len(list(Query(conn, (DataObjectMeta, DataObject)).filter(DataObjectMeta.name == my_avu[0]))) + + # Attach an AVU, then delete it. For both operations, check that a query gives the expected number of result rows. + avu_operation(conn, irods_object_type.DATA_OBJECT, "add", PATH, my_avu) + assert(my_avu_occurrences() == 1) + + avu_operation(conn, irods_object_type.DATA_OBJECT, "rm", PATH, my_avu) + assert(my_avu_occurrences() == 0) + + finally: + if desc is not None: + data_unlink(conn, PATH, force=True) diff --git a/fprc/irods/client/pytests/test_imports.py b/fprc/irods/client/pytests/test_imports.py new file mode 100644 index 000000000..157ccecef --- /dev/null +++ b/fprc/irods/client/pytests/test_imports.py @@ -0,0 +1,25 @@ +from os.path import abspath, dirname, join +import sys + +verbose = False + +def do_imports(): + import irods.client + import irods.client.utility.password_obfuscation + import irods.client.low_level.message.property_types + import irods.client.low_level.client_server_negotiation + import irods.client.low_level.message.ordered + import irods.client.low_level.message.property_types + import irods.client.low_level.message.quasixml + import irods.client.low_level.message.message + import irods.client.low_level.keywords + import irods.client.low_level.exception + + return irods.client.low_level.message.ET() + +def test_et(): + assert do_imports() is not None + +if __name__ == '__main__': + verbose = True + print(do_imports()) diff --git a/fprc/irods/client/query/__init__.py b/fprc/irods/client/query/__init__.py new file mode 100644 index 000000000..a41ffb1ed --- /dev/null +++ b/fprc/irods/client/query/__init__.py @@ -0,0 +1,369 @@ +from collections import OrderedDict +from typing import Sequence + + +from ...server_constants import MAX_SQL_ROWS + +from ..low_level.message import ( + IntegerIntegerMap, + IntegerStringMap, + StringStringMap, + _OrderedMultiMapping, + GenQueryRequest, + GenQueryResponse, + empty_gen_query_out, + iRODSMessage, + SpecificQueryRequest, + GeneralAdminRequest, +) + +from ...api_number import api_number +from ..low_level.exception import CAT_NO_ROWS_FOUND, MultipleResultsFound, NoResultFound +from .models import * +from .results import ResultSet, SpecificQueryResultSet +from .models import Model +from .column import Column, Column_remover, Keyword + +query_number = { + "ORDER_BY": 0x400, + "ORDER_BY_DESC": 0x800, + "RETURN_TOTAL_ROW_COUNT": 0x20, + "NO_DISTINCT": 0x40, + "QUOTA_QUERY": 0x80, + "AUTO_CLOSE": 0x100, + "UPPER_CASE_WHERE": 0x200, + "SELECT_MIN": 2, + "SELECT_MAX": 3, + "SELECT_SUM": 4, + "SELECT_AVG": 5, + "SELECT_COUNT": 6, +} + +IRODS_QUERY_LIMIT = 500 + + +class Query: + def __init__(self, conn, column_args: Sequence[Model|Column|Column_remover], /, **kwargs): + self.conn = conn + self.columns = OrderedDict() + self.criteria = [] + self.case_sensitive = kwargs.pop("case_sensitive", True) + self._limit = -1 + self._offset = 0 + self._continue_index = 0 + self._keywords = {} + self._column_args = column_args + + for arg in column_args: + if isinstance(arg, type) and issubclass(arg, Model): + for col in arg._columns: +# DWM revisit: if self.sess.server_version >= col.min_version: + self.columns[col] = 1 + elif isinstance(arg, Column): + self.columns[arg] = 1 + elif isinstance(arg, Column_remover): + self.columns.pop(arg.column, None) + else: + raise TypeError("Arguments must be models or columns") + + def _clone(self): + new_q = Query(self.conn, self._column_args) + new_q.columns = self.columns + new_q.criteria = self.criteria + new_q.case_sensitive = self.case_sensitive + new_q._limit = self._limit + new_q._offset = self._offset + new_q._continue_index = self._continue_index + new_q._keywords = self._keywords + return new_q + + def add_keyword(self, keyword, value=""): + new_q = self._clone() + new_q._keywords[keyword] = value + return new_q + + def filter(self, *criteria): + new_q = self._clone() + + if self.case_sensitive: + new_q.criteria += list(criteria) + else: + # In case-insensitive mode, all criterion values are converted + # to uppercase here, and the UPPER_CASE_WHERE option is enabled + # when creating a new GenQueryRequest in the _message function. + # Converting both keys and values to uppercase results in + # case-insensitive queries. + for criterion in criteria: + if type(criterion.value) == str: + criterion.value = str.upper(criterion.value) + elif type(criterion.value) == list: + criterion.value = [str.upper(c) if type(c) == str else c for c in criterion.value] + elif type(criterion.value) == tuple: + criterion.value = tuple([str.upper(c) if type(c) == str else c for c in list(criterion.value)]) + new_q.criteria.append(criterion) + + return new_q + + def order_by(self, column, order="asc"): + new_q = self._clone() + new_q.columns.pop(column, None) + if order == "asc": + new_q.columns[column] = query_number["ORDER_BY"] + elif order == "desc": + new_q.columns[column] = query_number["ORDER_BY_DESC"] + else: + raise ValueError("Ordering must be 'asc' or 'desc'") + return new_q + + def limit(self, limit): + new_q = self._clone() + new_q._limit = limit + return new_q + + def offset(self, offset): + new_q = self._clone() + new_q._offset = offset + return new_q + + def continue_index(self, continue_index): + new_q = self._clone() + new_q._continue_index = continue_index + return new_q + + def _aggregate(self, func, *args): + new_q = self._clone() + # NOTE(wtakase): Override existing column by aggregation. + for arg in args: + if isinstance(arg, type) and issubclass(arg, Model): + for col in arg._columns: + self.columns[col] = func + elif isinstance(arg, Column): + self.columns[arg] = func + else: + raise TypeError("Arguments must be models or columns") + new_q.columns = self.columns + return new_q + + def min(self, *args): + return self._aggregate(query_number["SELECT_MIN"], *args) + + def max(self, *args): + return self._aggregate(query_number["SELECT_MAX"], *args) + + def sum(self, *args): + return self._aggregate(query_number["SELECT_SUM"], *args) + + def avg(self, *args): + return self._aggregate(query_number["SELECT_AVG"], *args) + + def count(self, *args): + return self._aggregate(query_number["SELECT_COUNT"], *args) + + def _select_message(self): + dct = OrderedDict([(column.icat_id, value) for (column, value) in self.columns.items()]) + return IntegerIntegerMap(dct) + + # todo store criterion for columns and criterion for keywords in seaparate + # lists + def _conds_message(self): + dct = _OrderedMultiMapping([ + ( + criterion.query_key.icat_id, + criterion.op + " " + criterion.irods_value, + ) + for criterion in self.criteria + if isinstance(criterion.query_key, Column) + ]) + return IntegerStringMap(dct) + + def _kw_message(self): + dct = dict([ + ( + criterion.query_key.icat_key, + criterion.op + " " + criterion.irods_value, + ) + for criterion in self.criteria + if isinstance(criterion.query_key, Keyword) + ]) + for key in self._keywords: + dct[key] = self._keywords[key] + return StringStringMap(dct) + + def _message(self): + max_rows = IRODS_QUERY_LIMIT if self._limit < 0 else self._limit + args = { + "maxRows": max_rows, + "continueInx": self._continue_index, + "partialStartIndex": self._offset, + "options": 0 if self.case_sensitive else query_number["UPPER_CASE_WHERE"], + "KeyValPair_PI": self._kw_message(), + "InxIvalPair_PI": self._select_message(), + "InxValPair_PI": self._conds_message(), + } + return GenQueryRequest(**args) + + def execute(self): + conn = self.conn + message_body = self._message() + message = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GEN_QUERY_AN"]) + + conn.send(message) + try: + result_message = conn.recv() + results = result_message.get_main_message(GenQueryResponse) + result_set = ResultSet(results) + except CAT_NO_ROWS_FOUND: + result_set = ResultSet(empty_gen_query_out(list(self.columns.keys()))) + return result_set + + def close(self): + """Closes an open query on the server side. + self._continue_index must be set to a valid value (returned by a previous query API call). + """ + self.limit(0).execute() + + def _all(self): + """Internally used version of all(). Unlike the public version, the returned iterator when + executed to completion is unaffected by IRODS_QUERY_LIMIT. This is in order to accurately + reflect the enumeration of sub-objects, e.g. AVU's associated with an object. + """ + return self.get_results() + + def all(self): + result_set = self.execute() + if result_set.continue_index > 0: + self.continue_index(result_set.continue_index).close() + return result_set + + def get_batches(self): + result_set = self.execute() + + try: + yield result_set + + while result_set.continue_index > 0: + try: + result_set = self.continue_index(result_set.continue_index).execute() + yield result_set + except CAT_NO_ROWS_FOUND: + break + except GeneratorExit: + if result_set.continue_index > 0: + self.continue_index(result_set.continue_index).close() + + def get_results(self): + for result_set in self.get_batches(): + for result in result_set: + yield result + + def __iter__(self): + return self.get_results() + + def one(self): + results = self.execute() + if results.continue_index > 0: + self.continue_index(results.continue_index).close() + if not len(results): + raise NoResultFound() + if len(results) > 1: + raise MultipleResultsFound() + return results[0] + + def first(self): + query = self.limit(1) + results = query.execute() + if results.continue_index > 0: + query.continue_index(results.continue_index).close() + if not len(results): + return None + else: + return results[0] + + +# def __getitem__(self, val): +# pass + + +class SpecificQuery: + def __init__(self, sess, sql=None, alias=None, columns=None, args=None): + if not sql and not alias: + raise ValueError("A query or alias must be provided") + + self.session = sess + self._sql = sql + self._alias = alias + self._continue_index = 0 + self._columns = columns + self._args = args or [] + + def register(self): + if not self._sql: + raise ValueError("Empty query") + + message_body = GeneralAdminRequest("add", "specificQuery", self._sql, self._alias) + request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) + + with self.session.pool.get_connection() as conn: + conn.send(request) + response = conn.recv() + return response + + def remove(self): + target = self._alias or self._sql + + message_body = GeneralAdminRequest("rm", "specificQuery", target) + request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["GENERAL_ADMIN_AN"]) + + with self.session.pool.get_connection() as conn: + conn.send(request) + response = conn.recv() + return response + + def execute(self, limit=MAX_SQL_ROWS, offset=0, options=0, conditions=None): + target = self._alias or self._sql + + if conditions is None: + conditions = StringStringMap({}) + + sql_args = {} + for i, arg in enumerate(self._args[:10], start=1): + sql_args["arg{}".format(i)] = arg + + message_body = SpecificQueryRequest( + sql=target, + maxRows=limit, + continueInx=self._continue_index, + rowOffset=offset, + options=0, + KeyValPair_PI=conditions, + **sql_args, + ) + + request = iRODSMessage("RODS_API_REQ", msg=message_body, int_info=api_number["SPECIFIC_QUERY_AN"]) + + with self.session.pool.get_connection() as conn: + conn.send(request) + response = conn.recv() + + results = response.get_main_message(GenQueryResponse) + return SpecificQueryResultSet(results, self._columns) + + def __iter__(self): + return self.get_results() + + def get_batches(self): + result_set = self.execute() + yield result_set + + while result_set.continue_index > 0: + self._continue_index = result_set.continue_index + try: + result_set = self.execute() + yield result_set + except CAT_NO_ROWS_FOUND: + break + + def get_results(self): + for result_set in self.get_batches(): + for result in result_set: + yield result diff --git a/fprc/irods/client/query/column.py b/fprc/irods/client/query/column.py new file mode 100644 index 000000000..85305ef83 --- /dev/null +++ b/fprc/irods/client/query/column.py @@ -0,0 +1,166 @@ +from datetime import datetime, timezone +from calendar import timegm + + +class Column_remover: + def __init__(self, column): + self.column = column + + +class QueryKey: + def __init__(self, column_type): + self.column_type = column_type + + def __lt__(self, other): + return Criterion("<", self, other) + + def __le__(self, other): + return Criterion("<=", self, other) + + def __eq__(self, other): + return Criterion("=", self, other) + + def __ne__(self, other): + return Criterion("<>", self, other) + + def __gt__(self, other): + return Criterion(">", self, other) + + def __ge__(self, other): + return Criterion(">=", self, other) + + +class Criterion: + def __init__(self, op, query_key, value): + self.op = op + self.query_key = query_key + self.value = value + + @property + def irods_value(self): + return self.query_key.column_type.to_irods(self.value) + + +class In(Criterion): + def __init__(self, query_key, value): + super(In, self).__init__("in", query_key, value) + + @property + def irods_value(self): + v = "(" + comma = "" + for element in self.value: + v += "{}'{}'".format(comma, element) + comma = "," + v += ")" + return v + + +class Like(Criterion): + def __init__(self, query_key, value): + super(Like, self).__init__("like", query_key, value) + + +class NotLike(Criterion): + def __init__(self, query_key, value): + super(NotLike, self).__init__("not like", query_key, value) + + +class Between(Criterion): + def __init__(self, query_key, value): + super(Between, self).__init__("between", query_key, value) + + @property + def irods_value(self): + lower_bound, upper_bound = self.value + return "{} {}".format( + self.query_key.column_type.to_irods(lower_bound), + self.query_key.column_type.to_irods(upper_bound), + ) + + +class Column(QueryKey): + def __init__(self, column_type, icat_key, icat_id, min_version=(0, 0, 0)): + self.icat_key = icat_key + self.icat_id = icat_id + self.min_version = min_version + super(Column, self).__init__(column_type) + + def __neg__(self): + return Column_remover(self) + + def __repr__(self): + return "<{}.{} {} {}>".format( + self.__class__.__module__, + self.__class__.__name__, + self.icat_id, + self.icat_key, + ) + + @property + def id_tuple(self): + return self.column_type, self.icat_key, self.icat_id + + def __hash__(self): + return hash(self.id_tuple) + + def __eq__(self, other): + if isinstance(other, Column): + return self.id_tuple == other.id_tuple + return super().__eq__(other) + + +class Keyword(QueryKey): + def __init__(self, column_type, icat_key): + self.icat_key = icat_key + super(Keyword, self).__init__(column_type) + + +# consider renaming columnType +class ColumnType: + @staticmethod + def to_python(string): + pass + + @staticmethod + def to_irods(data): + pass + + +class Integer(ColumnType): + @staticmethod + def to_python(string): + return int(string) + + @staticmethod + def to_irods(data): + return "'{}'".format(data) + + +class String(ColumnType): + @staticmethod + def to_python(string): + return string + + @staticmethod + def to_irods(data): + try: + # Convert to Unicode string (aka decode) + data = str(data, "utf-8", "replace") + except TypeError: + # Some strings are already Unicode so they do not need decoding + pass + return "'{}'".format(data) + + +class DateTime(ColumnType): + @staticmethod + def to_python(string): + return datetime.fromtimestamp(int(string), timezone.utc) + + @staticmethod + def to_irods(data): + try: + return "'{:0>11}'".format(timegm(data.utctimetuple())) + except AttributeError: + return "'{}'".format(data) diff --git a/fprc/irods/client/query/models.py b/fprc/irods/client/query/models.py new file mode 100644 index 000000000..67981c5b8 --- /dev/null +++ b/fprc/irods/client/query/models.py @@ -0,0 +1,292 @@ +from typing import Dict, List, Tuple +from .column import Column, Integer, String, DateTime, Keyword + +__all__ = [ +"RuleExec", +] + +class ModelBase(type): + column_items: List[Tuple[int, Column]] = [] + column_dict: Dict[int, Column] = {} + + @classmethod + def columns(cls): + if not cls.column_dict: + cls.column_dict = dict(cls.column_items) + return cls.column_dict + + def __new__(cls, name, bases, attr): + column_objects = [y for (x, y) in attr.items() if isinstance(y, Column)] + for col in column_objects: + ModelBase.column_items.append((col.icat_id, col)) + attr["_columns"] = column_objects + return type.__new__(cls, name, bases, attr) + + +class Model(metaclass=ModelBase): + pass + + +class RuleExec(Model): + id = Column(Integer, "RULE_EXEC_ID", 1000) + name = Column(String, "RULE_EXEC_NAME", 1001) + rei_file_path = Column(String, "RULE_EXEC_REI_FILE_PATH", 1002) + user_name = Column(String, "RULE_EXEC_USER_NAME", 1003) + time = Column(DateTime, "RULE_EXEC_TIME", 1005) + last_exe_time = Column(DateTime, "RULE_EXEC_LAST_EXE_TIME", 1010) + frequency = Column(String, "RULE_EXEC_FREQUENCY", 1006) + priority = Column(String, "RULE_EXEC_PRIORITY", 1007) + context = Column(String, 'RULE_EXEC_CONTEXT', 1012, min_version=(4, 3, 0)) + +# # These are either unused or usually absent: +# exec_status = Column(String,'RULE_EXEC_STATUS', 1011) +# address = Column(String,'RULE_EXEC_ADDRESS', 1004) +# notification_addr = Column('RULE_EXEC_NOTIFICATION_ADDR', 1009) + + +class Zone(Model): + id = Column(Integer, "ZONE_ID", 101) + name = Column(String, "ZONE_NAME", 102) + type = Column(String, "ZONE_TYPE", 103) + connection = Column(String, "ZONE_CONNECTION", 104) + comment = Column(String, "ZONE_COMMENT", 105) + + +class User(Model): + id = Column(Integer, "USER_ID", 201) + name = Column(String, "USER_NAME", 202) + type = Column(String, "USER_TYPE", 203) + zone = Column(String, "USER_ZONE", 204) + info = Column(String, "USER_INFO", 206) + comment = Column(String, "USER_COMMENT", 207) + create_time = Column(DateTime, "USER_CREATE_TIME", 208) + modify_time = Column(DateTime, "USER_MODIFY_TIME", 209) + + +class UserAuth(Model): + user_id = Column(Integer, "USER_AUTH_ID", 1600) + user_dn = Column(String, "USER_DN", 1601) + + +class CollectionUser(Model): + name = Column(String, "COL_COLL_USER_NAME", 1300) + zone = Column(String, "COL_COLL_USER_ZONE", 1301) + + +class Group(Model): + id = Column(Integer, "USER_GROUP_ID", 900) + name = Column(String, "USER_GROUP_NAME", 901) + + +class Resource(Model): + id = Column(Integer, "R_RESC_ID", 301) + name = Column(String, "R_RESC_NAME", 302) + zone_name = Column(String, "R_ZONE_NAME", 303) + type = Column(String, "R_TYPE_NAME", 304) + class_name = Column(String, "R_CLASS_NAME", 305) + location = Column(String, "R_LOC", 306) + vault_path = Column(String, "R_VAULT_PATH", 307) + free_space = Column(String, "R_FREE_SPACE", 308) + free_space_time = Column(String, "R_FREE_SPACE_TIME", 314, min_version=(4, 0, 0)) + comment = Column(String, "R_RESC_COMMENT", 310) + create_time = Column(DateTime, "R_CREATE_TIME", 311) + modify_time = Column(DateTime, "R_MODIFY_TIME", 312) + status = Column(String, "R_RESC_STATUS", 313, min_version=(4, 0, 0)) + children = Column(String, "R_RESC_CHILDREN", 315, min_version=(4, 0, 0)) + context = Column(String, "R_RESC_CONTEXT", 316, min_version=(4, 0, 0)) + parent = Column(String, "R_RESC_PARENT", 317, min_version=(4, 0, 0)) + parent_context = Column(String, "R_RESC_PARENT_CONTEXT", 318, min_version=(4, 2, 0)) + + +class DataObject(Model): + id = Column(Integer, "D_DATA_ID", 401) + collection_id = Column(Integer, "D_COLL_ID", 402) + name = Column(String, "DATA_NAME", 403) # basename + replica_number = Column(Integer, "DATA_REPL_NUM", 404) + version = Column(String, "DATA_VERSION", 405) + type = Column(String, "DATA_TYPE_NAME", 406) + size = Column(Integer, "DATA_SIZE", 407) + resource_name = Column(String, "D_RESC_NAME", 409) + path = Column(String, "D_DATA_PATH", 410) # physical path on resource + owner_name = Column(String, "D_OWNER_NAME", 411) + owner_zone = Column(String, "D_OWNER_ZONE", 412) + replica_status = Column(String, "D_REPL_STATUS", 413) + status = Column(String, "D_DATA_STATUS", 414) + checksum = Column(String, "D_DATA_CHECKSUM", 415) + expiry = Column(String, "D_EXPIRY", 416) + map_id = Column(Integer, "D_MAP_ID", 417) + comments = Column(String, "D_COMMENTS", 418) + create_time = Column(DateTime, "D_CREATE_TIME", 419) + modify_time = Column(DateTime, "D_MODIFY_TIME", 420) + resc_hier = Column(String, "D_RESC_HIER", 422, min_version=(4, 0, 0)) + resc_id = Column(String, "D_RESC_ID", 423, min_version=(4, 2, 0)) + access_time = Column(DateTime, "D_ACCESS_TIME", 424, min_version=(5, 0, 0)) + + +class Collection(Model): + id = Column(Integer, "COLL_ID", 500) + name = Column(String, "COLL_NAME", 501) + parent_name = Column(String, "COLL_PARENT_NAME", 502) + owner_name = Column(String, "COLL_OWNER_NAME", 503) + owner_zone = Column(String, "COLL_OWNER_ZONE", 504) + map_id = Column(String, "COLL_MAP_ID", 505) + inheritance = Column(String, "COLL_INHERITANCE", 506) + comments = Column(String, "COLL_COMMENTS", 507) + create_time = Column(DateTime, "COLL_CREATE_TIME", 508) + modify_time = Column(DateTime, "COLL_MODIFY_TIME", 509) + + +class DataObjectMeta(Model): + id = Column(String, "COL_META_DATA_ATTR_ID", 603) + name = Column(String, "COL_META_DATA_ATTR_NAME", 600) + value = Column(String, "COL_META_DATA_ATTR_VALUE", 601) + units = Column(String, "COL_META_DATA_ATTR_UNITS", 602) + create_time = Column(DateTime, "COL_META_DATA_CREATE_TIME", 604) + modify_time = Column(DateTime, "COL_META_DATA_MODIFY_TIME", 605) + + +class CollectionMeta(Model): + id = Column(String, "COL_META_COLL_ATTR_ID", 613) + name = Column(String, "COL_META_COLL_ATTR_NAME", 610) + value = Column(String, "COL_META_COLL_ATTR_VALUE", 611) + units = Column(String, "COL_META_COLL_ATTR_UNITS", 612) + create_time = Column(DateTime, "COL_META_COLL_CREATE_TIME", 614) + modify_time = Column(DateTime, "COL_META_COLL_MODIFY_TIME", 615) + + +class ResourceMeta(Model): + id = Column(String, "COL_META_RESC_ATTR_ID", 633) + name = Column(String, "COL_META_RESC_ATTR_NAME", 630) + value = Column(String, "COL_META_RESC_ATTR_VALUE", 631) + units = Column(String, "COL_META_RESC_ATTR_UNITS", 632) + create_time = Column(DateTime, "COL_META_RESC_CREATE_TIME", 634) + modify_time = Column(DateTime, "COL_META_RESC_MODIFY_TIME", 635) + + +class UserMeta(Model): + id = Column(String, "COL_META_USER_ATTR_ID", 643) + name = Column(String, "COL_META_USER_ATTR_NAME", 640) + value = Column(String, "COL_META_USER_ATTR_VALUE", 641) + units = Column(String, "COL_META_USER_ATTR_UNITS", 642) + create_time = Column(DateTime, "COL_META_USER_CREATE_TIME", 644) + modify_time = Column(DateTime, "COL_META_USER_MODIFY_TIME", 645) + + +class DataAccess(Model): + type = Column(Integer, "DATA_ACCESS_TYPE", 700) + name = Column(String, "COL_DATA_ACCESS_NAME", 701) + token_namespace = Column(String, "COL_DATA_TOKEN_NAMESPACE", 702) + user_id = Column(Integer, "COL_DATA_ACCESS_USER_ID", 703) + data_id = Column(Integer, "COL_DATA_ACCESS_DATA_ID", 704) + + +class CollectionAccess(Model): + type = Column(Integer, "COL_COLL_ACCESS_TYPE", 710) + name = Column(String, "COL_COLL_ACCESS_NAME", 711) + token_namespace = Column(String, "COL_COLL_TOKEN_NAMESPACE", 712) + user_id = Column(Integer, "COL_COLL_ACCESS_USER_ID", 713) + access_id = Column(Integer, "COL_COLL_ACCESS_COLL_ID", 714) + + +class SpecificQueryResult(Model): + """ + To parse results of specific queries. No corresponding iCAT table. + """ + + value = Column(String, "SQL_RESULT_VALUE", 0) + + +# not really a model. Should be dict instead? +class Keywords(Model): + data_type = Keyword(String, "dataType") + chksum = Keyword(String, "chksum") + + +class Quota(Model): + user_id = Column(Integer, "COL_QUOTA_USER_ID", 2000) + resc_id = Column(Integer, "COL_QUOTA_RESC_ID", 2001) + limit = Column(Integer, "COL_QUOTA_LIMIT", 2002) + over = Column(Integer, "COL_QUOTA_OVER", 2003) + modify_time = Column(DateTime, "COL_QUOTA_MODIFY_TIME", 2004) + + +class TicketQuery: + """Various model classes for querying attributes of iRODS tickets. + + Namespacing these model classes under the TicketQuery parent class allows + a simple import (not conflicting with irods.ticket.Ticket) and a usage + that reflects ICAT table structure: + + from irods.models import TicketQuery + # ... + for row in session.query( TicketQuery.Ticket )\ + .filter( TicketQuery.Owner.name == 'alice' ): + print( row [TicketQuery.Ticket.string] ) + + (For more examples, see irods/test/ticket_test.py) + + """ + + class Ticket(Model): + """For queries of R_TICKET_MAIN.""" + + id = Column(Integer, "TICKET_ID", 2200) + string = Column(String, "TICKET_STRING", 2201) + type = Column(String, "TICKET_TYPE", 2202) + user_id = Column(Integer, "TICKET_USER_ID", 2203) + object_id = Column(Integer, "TICKET_OBJECT_ID", 2204) + object_type = Column(String, "TICKET_OBJECT_TYPE", 2205) + uses_limit = Column(Integer, "TICKET_USES_LIMIT", 2206) + uses_count = Column(Integer, "TICKET_USES_COUNT", 2207) + expiry_ts = Column(String, "TICKET_EXPIRY_TS", 2208) + write_file_count = Column(Integer, "TICKET_WRITE_FILE_COUNT", 2211) + write_file_limit = Column(Integer, "TICKET_WRITE_FILE_LIMIT", 2212) + write_byte_count = Column(Integer, "TICKET_WRITE_BYTE_COUNT", 2213) + write_byte_limit = Column(Integer, "TICKET_WRITE_BYTE_LIMIT", 2214) + + create_time = Column(DateTime, 'TICKET_CREATE_TIME', 2209, min_version=(4, 3, 0)) + modify_time = Column(DateTime, 'TICKET_MODIFY_TIME', 2210, min_version=(4, 3, 0)) + + class DataObject(Model): + """For queries of R_DATA_MAIN when joining to R_TICKET_MAIN. + + The ticket(s) in question should be for a data object; otherwise + CAT_SQL_ERR is thrown. + + """ + + name = Column(String, "TICKET_DATA_NAME", 2226) + coll = Column(String, "TICKET_DATA_COLL_NAME", 2227) + + class Collection(Model): + """For queries of R_COLL_MAIN when joining to R_TICKET_MAIN. + + The returned ticket(s) will be limited to those issued for collections. + + """ + + name = Column(String, "TICKET_COLL_NAME", 2228) + + class Owner(Model): + """For queries concerning R_TICKET_USER_MAIN.""" + + name = Column(String, "TICKET_OWNER_NAME", 2229) + zone = Column(String, "TICKET_OWNER_ZONE", 2230) + + class AllowedHosts(Model): + """For queries concerning R_TICKET_ALLOWED_HOSTS.""" + + ticket_id = Column(String, "COL_TICKET_ALLOWED_HOST_TICKET_ID", 2220) + host = Column(String, "COL_TICKET_ALLOWED_HOST", 2221) + + class AllowedUsers(Model): + """For queries concerning R_TICKET_ALLOWED_USERS.""" + + ticket_id = Column(String, "COL_TICKET_ALLOWED_USER_TICKET_ID", 2222) + user_name = Column(String, "COL_TICKET_ALLOWED_USER_NAME", 2223) + + class AllowedGroups(Model): + """For queries concerning R_TICKET_ALLOWED_GROUPS.""" + + ticket_id = Column(String, "COL_TICKET_ALLOWED_GROUP_TICKET_ID", 2224) + group_name = Column(String, "COL_TICKET_ALLOWED_GROUP_NAME", 2225) diff --git a/fprc/irods/client/query/results.py b/fprc/irods/client/query/results.py new file mode 100644 index 000000000..abd2e9aaf --- /dev/null +++ b/fprc/irods/client/query/results.py @@ -0,0 +1,92 @@ +from .models import ModelBase + + +class ResultSet: + def __init__(self, raw): + self.length = raw.rowCnt + col_length = raw.attriCnt + self.cols = raw.SqlResult_PI[:col_length] + self.rows = [self._format_row(i) for i in range(self.length)] + try: + self.continue_index = raw.continueInx + except KeyError: + self.continue_index = 0 + + def __str__(self): + from prettytable import PrettyTable + + table = PrettyTable() + for col in self.cols: + table.add_column(ModelBase.columns()[col.attriInx].icat_key, col.value) + table.align = "l" + return table.get_string() + + def get_html_string(self, *args, **kwargs): + from prettytable import PrettyTable + + table = PrettyTable() + for col in self.cols: + table.add_column(ModelBase.columns()[col.attriInx].icat_key, col.value) + table.align = "l" + return table.get_html_string(*args, **kwargs) + + @staticmethod + def _format_attribute(attribute_index, value): + col = ModelBase.columns()[attribute_index] + try: + return (col, col.column_type.to_python(value)) + except (TypeError, ValueError): + return (col, value) + + _get_column_values = lambda self, index: [(col, col.value[index]) for col in self.cols] + + def _format_row(self, index): + values = self._get_column_values(index) + return dict([self._format_attribute(col.attriInx, value) for col, value in values]) + + def __getitem__(self, index): + return self.rows.__getitem__(index) + + def __iter__(self): + return self.rows.__iter__() + + def __len__(self): + return self.length + + # For testing. Might go somewhere else... + def has_value(self, value): + found = False + + for row in self.rows: + if value in list(row.values()): + found = True + + return found + + +class SpecificQueryResultSet(ResultSet): + def __init__(self, raw, columns=None): + self._query_columns = columns + super(SpecificQueryResultSet, self).__init__(raw) + + def _format_row(self, index): + values = [col.value[index] for col in self.cols] + + formatted_row = {} + + for i, value in enumerate(values): + try: + column = self._query_columns[i] + result_key = column + except TypeError: + column = ModelBase.columns()[0] # SpecificQueryResult.value + result_key = i + + try: + formatted_value = column.column_type.to_python(value) + except (TypeError, ValueError): + formatted_value = value + + formatted_row[result_key] = formatted_value + + return formatted_row diff --git a/fprc/irods/client/utility/__init__.py b/fprc/irods/client/utility/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fprc/irods/client/utility/password_obfuscation.py b/fprc/irods/client/utility/password_obfuscation.py new file mode 100644 index 000000000..b68ff261b --- /dev/null +++ b/fprc/irods/client/utility/password_obfuscation.py @@ -0,0 +1,402 @@ +import hashlib +import sys +import os +import time +import random +import string +from ...server_constants import MAX_PASSWORD_LENGTH + +seq_list = [ + 0xD768B678, + 0xEDFDAF56, + 0x2420231B, + 0x987098D8, + 0xC1BDFEEE, + 0xF572341F, + 0x478DEF3A, + 0xA830D343, + 0x774DFA2A, + 0x6720731E, + 0x346FA320, + 0x6FFDF43A, + 0x7723A320, + 0xDF67D02E, + 0x86AD240A, + 0xE76D342E, +] + +# Don't forget to drink your Ovaltine +wheel = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "!", + '"', + "#", + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", +] + +default_password_key = "a9_3fker" +default_scramble_prefix = ".E_" +v2_prefix = "A.ObfV2" + + +def str_to_int(input_string): + """Convert `input_string` into an integer. + + Parameters + ---------- + input_string : str + Value to convert. + + Returns + ------- + int + Converted value. + """ + return sum((ord(char) for char in input_string)) + + +# Decode a password from a .irodsA file +def decode(s, uid=None): + # This value lets us know which seq value to use + # Referred to as "rval" in the C code + seq_index = ord(s[6]) - ord("e") + seq = seq_list[seq_index] + + # How much we bitshift seq by when we use it + # Referred to as "addin_i" in the C code + # Since we're skipping five bytes that are normally read, + # we start at 15 + bitshift = 15 + + # The uid is used as a salt. + if uid is None: + try: + uid = os.getuid() + except AttributeError: + # Spoof UID for Non-POSIX + uid = str_to_int(os.getlogin()) + + # The first byte is a dot, the next five are literally irrelevant + # garbage, and we already used the seventh one. The string to decode + # starts at byte eight. + encoded_string = s[7:] + + decoded_string = "" + + for c in encoded_string: + if ord(c) == 0: + break + # How far this character is from the target character in wheel + # Referred to as "add_in" in the C code + offset = ((seq >> bitshift) & 0x1F) + (uid & 0xF5F) + + bitshift += 3 + if bitshift > 28: + bitshift = 0 + + # The character is only encoded if it's one of the ones in wheel + if c in wheel: + # index of the target character in wheel + wheel_index = (len(wheel) + wheel.index(c) - offset) % len(wheel) + decoded_string += wheel[wheel_index] + else: + decoded_string += c + + return decoded_string + + +# encode passwords to store in the .irodsA file +def encode(s, uid=None, mtime=None): + # mtime & 65535 needs to be within 20 seconds of the + # .irodsA file's mtime & 65535 + if mtime is None: + mtime = int(time.time()) + + # How much we bitshift seq by when we use it + # Referred to as "addin_i" in the C code + # We can't skip the first five bytes this time, + # so we start at 0 + bitshift = 0 + + # The uid is used as a salt. + if uid is None: + try: + uid = os.getuid() + except AttributeError: + # Spoof UID for Non-POSIX + uid = str_to_int(os.getlogin()) + + # This value lets us know which seq value to use + # Referred to as "rval" in the C code + # The C code is very specific about this being mtime & 15, + # but it's never checked. Let's use zero. + seq_index = 0 + seq = seq_list[seq_index] + + to_encode = "" + + # The C code DOES really care about this value matching + # the seq_index, though + to_encode += chr(ord("S") - ((seq_index & 0x7) * 2)) + + # And this is also a song and dance to + # convince the C code we are legitimate + to_encode += chr(((mtime >> 4) & 0xF) + ord("a")) + to_encode += chr((mtime & 0xF) + ord("a")) + to_encode += chr(((mtime >> 12) & 0xF) + ord("a")) + to_encode += chr(((mtime >> 8) & 0xF) + ord("a")) + + # We also want to actually encode the passed string + to_encode += s + + # Yeah, the string starts with a dot. Whatever. + encoded_string = "." + + for c in to_encode: + if ord(c) == 0: + break + + # How far this character is from the target character in wheel + # Referred to as "add_in" in the C code + offset = ((seq >> bitshift) & 0x1F) + (uid & 0xF5F) + + bitshift += 3 + if bitshift > 28: + bitshift = 0 + + # The character is only encoded if it's one of the ones in wheel + if c in wheel: + # index of the target character in wheel + wheel_index = (wheel.index(c) + offset) % len(wheel) + encoded_string += wheel[wheel_index] + else: + encoded_string += c + + # insert the seq_index (which is NOT encoded): + encoded_string = chr(seq_index + ord("e")).join([encoded_string[:6], encoded_string[6:]]) + + # aaaaand, append a null character. because we want to print + # a null character to the file. because that's a good idea. + encoded_string += chr(0) + + return encoded_string + + +# Hash some stuff to create ANOTHER encoder ring. +def get_encoder_ring(key=default_password_key): + + md5_hasher = hashlib.md5() + # key (called keyBuf in the C) is padded + # or truncated to 100 characters + md5_hasher.update(key.ljust(100, chr(0))[:100].encode("ascii")) + first = md5_hasher.digest() + + md5_hasher = hashlib.md5() + md5_hasher.update(first) + second = md5_hasher.digest() + + md5_hasher = hashlib.md5() + md5_hasher.update(first + second) + third = md5_hasher.digest() + + return first + second + third + third + + +# unscramble passwords stored in the database +def unscramble( + s, + key=default_password_key, + scramble_prefix=default_scramble_prefix, + block_chaining=False, +): + if key is None: + key = default_password_key + + if not s.startswith(scramble_prefix): + # not scrambled. or if it is, not + # in a way we can unscramble + return s + + # the prefix is nonsense, strip it off + to_unscramble = s[len(scramble_prefix) :] + + # get our encoder ring (called cpKey in the C code) + encoder_ring = get_encoder_ring(key) + encoder_ring_index = 0 + + # for block chaining + chain = 0 + + unscrambled_string = "" + for c in to_unscramble: + if c in wheel: + # the index of the target character in wheel + wheel_index = (wheel.index(c) - encoder_ring[encoder_ring_index % 61] - chain) % len(wheel) + unscrambled_string += wheel[wheel_index] + if block_chaining: + chain = ord(c) & 0xFF + else: + unscrambled_string += c + encoder_ring_index += 1 + + return unscrambled_string + + +# scramble passwords to store in the database +def scramble( + s, + key=default_password_key, + scramble_prefix=default_scramble_prefix, + block_chaining=False, +): + if key is None: + key = default_password_key + + to_scramble = s + + # get our encoder ring (called cpKey in the C code) + encoder_ring = get_encoder_ring(key) + encoder_ring_index = 0 + + # for block chaining + chain = 0 + + scrambled_string = "" + for c in to_scramble: + if c in wheel: + # the index of the target character in wheel + wheel_index = (wheel.index(c) + encoder_ring[encoder_ring_index % 61] + chain) % len(wheel) + scrambled_string += wheel[wheel_index] + if block_chaining: + chain = ord(scrambled_string[-1]) & 0xFF + else: + scrambled_string += c + encoder_ring_index += 1 + + return scramble_prefix + scrambled_string + + +# port of https://github.com/irods/irods/blob/4-2-stable/lib/core/src/obf.cpp#L1113 +def scramble_v2(s, first_key, second_key): + + to_scramble = random.SystemRandom().choice(string.printable) + v2_prefix[1:10] + s[:150] + + key = first_key[:90] + second_key[:100] + + # Must pad the key with null bytes to generate the same hash as the server code + # https://github.com/irods/irods/blob/4.2.1/lib/core/src/obf.cpp#L1303-L1314 + if sys.version_info > (2, 7, 7): + key = "{:\x00<100}".format(key) + else: + # https://bugs.python.org/issue12546 + key += "\x00" * max(100 - len(key), 0) + + md5_hasher = hashlib.md5() + md5_hasher.update(key.encode("utf-8")) + hashed_key = md5_hasher.hexdigest() + + return scramble(to_scramble, key=hashed_key, scramble_prefix="", block_chaining=True) + + +def obfuscate_new_password_with_key(new_password, obfuscation_key): + lcopy = MAX_PASSWORD_LENGTH - 10 - len(new_password) + new_password = new_password[:MAX_PASSWORD_LENGTH] + if lcopy > 15: + # https://github.com/irods/irods/blob/4.2.1/plugins/database/src/db_plugin.cpp#L1094-L1095 + padding = "1gCBizHWbwIYyWLoysGzTe6SyzqFKMniZX05faZHWAwQKXf6Fs" + new_password += padding[:lcopy] + return scramble(new_password, obfuscation_key, scramble_prefix="") + + +# port of https://github.com/irods/irods_client_icommands/blob/4.2.1/src/iadmin.cpp#L878-L930 +def obfuscate_new_password(new, old, signature): + pwd_len = len(new) + new = new[:MAX_PASSWORD_LENGTH] + lcopy = MAX_PASSWORD_LENGTH - 10 - pwd_len + + if lcopy > 15: + # https://github.com/irods/irods/blob/4.2.1/plugins/database/src/db_plugin.cpp#L1094-L1095 + padding = "1gCBizHWbwIYyWLoysGzTe6SyzqFKMniZX05faZHWAwQKXf6Fs" + new = new + padding[:lcopy] + + return scramble_v2(new, old, signature) + + +def create_temp_password(temp_hash, source_password): + password = (temp_hash + source_password).ljust(100, chr(0)) + password_md5 = hashlib.md5(password.encode("utf-8")) + + # Return hexdigest + return password_md5.hexdigest() diff --git a/fprc/irods/connect_constants.py b/fprc/irods/connect_constants.py new file mode 100644 index 000000000..9ef84b66b --- /dev/null +++ b/fprc/irods/connect_constants.py @@ -0,0 +1,12 @@ +import sys +import logging +import os + +DEFAULT_CONNECTION_TIMEOUT = 120 +# https://stackoverflow.com/questions/45704243/value-of-c-pytime-t-in-python +MAXIMUM_CONNECTION_TIMEOUT = 9223372036 + +NATIVE_AUTH_SCHEME = "native" + + + diff --git a/fprc/irods/server_constants.py b/fprc/irods/server_constants.py new file mode 100644 index 000000000..cc19bd541 --- /dev/null +++ b/fprc/irods/server_constants.py @@ -0,0 +1,10 @@ +# Magic numbers defined in iRODS server code. + +MAX_PASSWORD_LENGTH = 50 +MAX_SQL_ATTR = 50 +MAX_PATH_ALLOWED = 1024 +MAX_NAME_LEN = MAX_PATH_ALLOWED + 64 +LONG_NAME_LEN = 256 +RESPONSE_LEN = 16 +CHALLENGE_LEN = 64 +MAX_SQL_ROWS = 256 diff --git a/fprc/setup.py b/fprc/setup.py new file mode 100644 index 000000000..d093e0127 --- /dev/null +++ b/fprc/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, find_packages + +setup( + name="fprc", + version="0.1.0", + packages=find_packages(), +)