# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0
"""This module implements classes that will setup the Condor security as needed
"""
import copy
import os
###############
#
# Base classes
#
###############
########################################
# This class contains the state of the
# Condor environment
#
# All info is in the state attribute
[docs]
class EnvState:
def __init__(self, filter):
# filter is a list of Condor variables to save
self.filter = filter
self.load()
#################################
# Restore back to what you found
# when creating this object
[docs]
def restore(self):
for condor_key in list(self.state.keys()):
env_key = "_CONDOR_%s" % condor_key
old_val = self.state[condor_key]
if old_val is not None:
os.environ[env_key] = old_val
else:
if os.environ.get(env_key):
del os.environ[env_key]
##########################################
# Load the environment state into
# Almost never called by the user
# It gets called automatically by __init__
[docs]
def load(self):
filter = self.filter
saved_state = {}
for condor_key in filter:
env_key = "_CONDOR_%s" % condor_key
if env_key in os.environ:
saved_state[condor_key] = os.environ[env_key]
else:
saved_state[condor_key] = None # unlike requests, we want to remember there was nothing
self.state = saved_state
########################################
# This class contains the state of the
# security Condor environment
[docs]
def convert_sec_filter(sec_filter):
filter = []
for context in list(sec_filter.keys()):
for feature in sec_filter[context]:
condor_key = f"SEC_{context}_{feature}"
filter.append(condor_key)
return filter
[docs]
class SecEnvState(EnvState):
def __init__(self, sec_filter):
# sec_filter is a dictionary of [contex]=[feature list]
EnvState.__init__(self, convert_sec_filter(sec_filter))
self.sec_filter = sec_filter
###############################################################
# This special value will unset any value and leave the default
# This is different than None, that will not do anything
UNSET_VALUE = "UNSET"
#############################################
# This class handle requests for ensuring
# the security state is in a particular state
[docs]
class SecEnvRequest:
def __init__(self, requests=None):
# requests is a dictionary of requests [context][feature]=VAL
# TODO: requests can be a self initializinf dictionary of dictionaries in PY3
self.requests = {}
if requests is not None:
for context in list(requests.keys()):
for feature in list(requests[context].keys()):
self.set(context, feature, requests[context][feature])
self.saved_state = None
##############################################
# Methods for accessing the requests
[docs]
def set(self, context, feature, value): # if value is None, remove the request
if value is not None:
if context not in self.requests:
self.requests[context] = {}
self.requests[context][feature] = value
elif context in self.requests:
if feature in self.requests[context]:
del self.requests[context][feature]
if len(list(self.requests[context].keys())) == 0:
del self.requests[context]
[docs]
def get(self, context, feature):
if context in self.requests:
if feature in self.requests[context]:
return self.requests[context][feature]
else:
return None
else:
return None
##############################################
# Methods for preserving the old state
[docs]
def has_saved_state(self):
return self.saved_state is not None
[docs]
def save_state(self):
if self.has_saved_state():
raise RuntimeError("There is already a saved state! Restore that first.")
filter = {}
for c in list(self.requests.keys()):
filter[c] = list(self.requests[c].keys())
self.saved_state = SecEnvState(filter)
[docs]
def restore_state(self):
if self.saved_state is None:
return # nothing to do
self.saved_state.restore()
self.saved_state = None
##############################################
# Methods for changing to the desired state
# you should call save_state before this one,
# if you want to ever get back
[docs]
def enforce_requests(self):
for context in list(self.requests.keys()):
for feature in list(self.requests[context].keys()):
condor_key = f"SEC_{context}_{feature}"
env_key = "_CONDOR_%s" % condor_key
val = self.requests[context][feature]
if val != UNSET_VALUE:
os.environ[env_key] = val
else:
# unset -> make sure it is not in the env after the call
if env_key in os.environ:
del os.environ[env_key]
return
########################################################################
#
# Security protocol handshake classes
# This are the most basic features users may want to change
#
########################################################################
CONDOR_CONTEXT_LIST = (
"DEFAULT",
"ADMINISTRATOR",
"NEGOTIATOR",
"CLIENT",
"OWNER",
"READ",
"WRITE",
"DAEMON",
"CONFIG",
"ADVERTISE_MASTER",
"ADVERTISE_STARTD",
"ADVERTISE_SCHEDD",
)
CONDOR_PROTO_FEATURE_LIST = ("AUTHENTICATION", "INTEGRITY", "ENCRYPTION", "NEGOTIATION")
CONDOR_PROTO_VALUE_LIST = ("NEVER", "OPTIONAL", "PREFERRED", "REQUIRED")
########################################
[docs]
class EnvProtoState(SecEnvState):
def __init__(self, filter=None):
if filter is not None:
# validate filter
for c in list(filter.keys()):
if c not in CONDOR_CONTEXT_LIST:
raise ValueError(f"Invalid contex '{c}'. Must be one of {CONDOR_CONTEXT_LIST}")
for f in filter[c]:
if f not in CONDOR_PROTO_FEATURE_LIST:
raise ValueError(f"Invalid feature '{f}'. Must be one of {CONDOR_PROTO_FEATURE_LIST}")
else:
# do not filter anything out... add all
filter = {}
for c in CONDOR_CONTEXT_LIST:
cfilter = []
for f in CONDOR_PROTO_FEATURE_LIST:
cfilter.append(f)
filter[c] = cfilter
SecEnvState.__init__(self, filter)
#########################################
# Same as SecEnvRequest, but check that
# the context and feature are related
# to the Condor protocol handling
[docs]
class ProtoRequest(SecEnvRequest):
[docs]
def set(self, context, feature, value): # if value is None, remove the request
if context not in CONDOR_CONTEXT_LIST:
raise ValueError("Invalid security context '%s'." % context)
if feature not in CONDOR_PROTO_FEATURE_LIST:
raise ValueError("Invalid authentication feature '%s'." % feature)
if value not in (CONDOR_PROTO_VALUE_LIST + (UNSET_VALUE,)):
raise ValueError("Invalid value type '%s'." % value)
SecEnvRequest.set(self, context, feature, value)
[docs]
def get(self, context, feature):
if context not in CONDOR_CONTEXT_LIST:
raise ValueError("Invalid security context '%s'." % context)
if feature not in CONDOR_PROTO_FEATURE_LIST:
raise ValueError("Invalid authentication feature '%s'." % feature)
return SecEnvRequest.get(self, context, feature)
########################################################################
#
# GSI specific classes classes
# Extend ProtoRequest
# These assume all the communication will be GSI or IDTOKENS authenticated
#
########################################################################
[docs]
class GSIRequest(ProtoRequest):
def __init__(self, x509_proxy=None, allow_fs=True, allow_idtokens=True, proto_requests=None):
if allow_idtokens:
auth_str = "IDTOKENS,GSI"
else:
auth_str = "GSI"
if allow_fs:
auth_str += ",FS"
# force either IDTOKENS or GSI authentication
if proto_requests is not None:
proto_requests = copy.deepcopy(proto_requests)
else:
proto_requests = {}
for context in CONDOR_CONTEXT_LIST:
if context not in proto_requests:
proto_requests[context] = {}
if "AUTHENTICATION" in proto_requests[context]:
auth_list = proto_requests[context]["AUTHENTICATION"].split(",")
if "GSI" not in auth_list:
if "IDTOKENS" not in auth_list:
raise ValueError("Must specify either IDTOKENS or GSI as one of the options")
else:
proto_requests[context]["AUTHENTICATION"] = auth_str
ProtoRequest.__init__(self, proto_requests)
self.allow_fs = allow_fs
self.x509_proxy_saved_state = None
if x509_proxy is None:
# if 'X509_USER_PROXY' not in os.environ:
# raise RuntimeError("x509_proxy not provided and env(X509_USER_PROXY) undefined")
x509_proxy = os.environ.get("X509_USER_PROXY")
# Here I should probably check if the proxy is valid
# To be implemented in a future release
self.x509_proxy = x509_proxy
##############################################
[docs]
def save_state(self):
if self.has_saved_state():
raise RuntimeError("There is already a saved state! Restore that first.")
if "X509_USER_PROXY" in os.environ:
self.x509_proxy_saved_state = os.environ["X509_USER_PROXY"]
else:
self.x509_proxy_saved_state = None # unlike requests, we want to remember there was nothing
ProtoRequest.save_state(self)
[docs]
def restore_state(self):
if self.saved_state is None:
return # nothing to do
ProtoRequest.restore_state(self)
if self.x509_proxy_saved_state is not None:
os.environ["X509_USER_PROXY"] = self.x509_proxy_saved_state
else:
del os.environ["X509_USER_PROXY"]
# unset, just to prevent bugs
self.x509_proxy_saved_state = None
##############################################
[docs]
def enforce_requests(self):
ProtoRequest.enforce_requests(self)
if self.x509_proxy:
os.environ["X509_USER_PROXY"] = self.x509_proxy