#!/usr/bin/env python """!Handles data restriction classes. Implements access control mechanisms for NOAA data. Although this was written for the NOAA Restricted Data (rstprod), it can be used for general access control. It is also more general than NOAA, so long as one correctly initializes the produtil.cluster module. The mechanism used depends on the cluster, due to varying capabilities throughout. Some do not implement access control mechanisms that are usable for the restricted data (such as NOAA Jet). For those systems, RstNoAccessControl is raised if one attempts to restrict a file.""" ##@var __all__ # List of symbols exported by "from produtil.rstprod import *" __all__= [ 'RestrictionClass', 'tag_rstprod', 'rstprod_tagger', 'make_rstprod_tagger' ] class RstprodError(Exception): """!The base class of all exceptions specific to the rstprod module""" class RstNoAccessControl(RstprodError): """!Raised when the cluster has no access control mechanisms.""" class RstBadGroup(RstprodError): """!Raised when a group's id or name could not be determined.""" import os, stat, grp import produtil.cluster, produtil.acl from produtil.acl import ACL, ACL_TYPE_ACCESS, ACL_TYPE_DEFAULT ##@var okay_mode # File permission bits (from the stat module) that are allowed to be # set on restricted access data. When Access Control List (ACL) based # access control is used, the group bits refer to the rstprod's # permissions in the ACL, rather than the owning group. okay_mode = stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR | \ stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP def acl_text_for_rstclass(groupname,mode): """!Generates the access control list for the specified restriction class (groupname) and nine bit access permissions (mode). @param groupname the restricted file unix group @param mode required access mode (world access will be removed even if it is present in mode)""" if not isinstance(mode,int): raise TypeError( 'In acl_text_for_rstclass, the mode must be the integer access mode, not a %s %s' %(type(groupname).__name__,repr(groupname))) imode=int(mode)&0o770 if not isinstance(groupname,str): raise TypeError( 'In acl_text_for_rstclass, the groupname must be the string name of a unix group, not a %s %s' %(type(groupname).__name__,repr(groupname))) return "u::%c%c%c,g::---,g:%s:%c%c%c,o::---,m::rwx" % ( ( 'r' if 0!=imode&stat.S_IRUSR else '-' ), ( 'w' if 0!=imode&stat.S_IWUSR else '-' ), ( 'x' if 0!=imode&stat.S_IXUSR else '-' ), groupname, ( 'r' if 0!=imode&stat.S_IRGRP else '-' ), ( 'w' if 0!=imode&stat.S_IWGRP else '-' ), ( 'x' if 0!=imode&stat.S_IXGRP else '-' ) ) class RestrictionClass(object): """!This is a python class intended to be used to automate restricting data to a specific restriction class using access control lists or group ownership Example: @code rc=RestrictionClass("rstprod") rc.restrict_file("/path/to/some/dangerous/file") @endcode It can also set the Default Access Control List if supplied a directory: @code rc.restrict_file("/path/to/some/dangerous/directory/") @endcode""" def __init__(self,group,use_acl=None,logger=None): """!Create a new RestrictionClass object for the specified group. @param group The group may be the string group name, or the numeric group id. @param use_acl If use_acl is unspecified, then produtil.cluster.use_acl_for_rstdata() is used to decide. @param logger a logging.Logger for log messages""" assert(use_acl is None) if use_acl is None: # We are being asked to automatically decide what type of # access control mechanism to use. if produtil.cluster.no_access_control(): raise RstNoAccessControl( "This cluster cannot be used for NOAA restricted data. It " "uses group quotas, so I cannot control access through " "group IDs. It does not have a functional access control " "list (ACL) mechanism, so I cannot use ACLs.") use_acl=produtil.cluster.use_acl_for_rstdata() self.__use_acl=bool(use_acl) if isinstance(group,str): self.__groupname=group try: grent=grp.getgrnam(group) self.__groupid=grent[2] except (EnvironmentError,KeyError) as e: raise RstBadGroup('%s: could not get group id for group: %s' %(group,str(e))) if not isinstance(self.__groupid,int): raise RstBadGroup( '%s: could not get group id for group. The grp.getgrnam' '(...)[2] returned something that was not an int: a %s %s' %(group,type(self.__groupid).__name__,repr(self.__groupid))) elif isinstance(group,int): try: grent=grp.getgrgid(group) self.__groupname=grent[0] self.__groupid=group except (EnvironmentError,KeyError) as e: raise RstBadGroup('%s: could not get group id for group: %s' %(group,str(e))) if not isinstance(self.__groupname,str): raise RstBadGroup( '%d: could not get group name for group. The grp.getgrgid' '(...)[0] returned something that was not an int: a %s %s' %(group,type(self.__groupid).__name__, repr(self.__groupid))) else: raise TypeError( "In produtil.rstprod.RestrictionClass.__init__, the group parameter must be the string group name or integer group id. You provided a %s %s" %(type(group).__name__,repr(group))) self.__allowed=stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR | \ stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP if use_acl: self.__acls=self.make_acl_dict() def make_acl_dict(self): """!Internal function that generates the ACL dictionary. @protected This is part of the internal implementation of RestrictionClass and should not be used directly. It returns a dict() that maps from integer permission to an ACL object that will set an access control list appropriate for that permission. The user and restriction group will match the old user and group permissions, but other groups will have no permissions, and the "world" permissions will be 0.""" acls=dict() mode=0o10 for IRUSR in ( 0, stat.S_IRUSR ): for IWUSR in ( 0, stat.S_IWUSR ): for IXUSR in ( 0, stat.S_IXUSR ): for IRGRP in ( 0, stat.S_IRGRP ): for IWGRP in ( 0, stat.S_IWGRP ): for IXGRP in ( 0, stat.S_IXGRP ): mode=IRUSR|IWUSR|IXUSR|IRGRP|IWGRP|IXGRP txt=acl_text_for_rstclass(self.groupname,mode) acl=ACL() acl.from_text(txt) acls[mode]=acl return acls @property def groupname(self): """!The name of the group used for the restriction class""" return self.__groupname @property def groupid(self): """!The numeric ID of the group used for the restriction class""" return self.__groupid @property def use_acl(self): """!True if ACLs are used for access permission, False if setgid and chgrp are used.""" return self.__use_acl def acl_for(self,st_mode): """!Returns an produtil.acl.ACL object for the specified access mode. Will raise an exception if self.use_acl is False. @param st_mode desired access mode""" imode = stat.S_IMODE(st_mode) amode = imode & self.__allowed # limit to allowed permissions return self.__acls[amode] def chgrp_restrict(self,target,st_mode,chown,chmod,logger): """!Internal function that uses chgrp to restrict a file's access. This is an internal implementation function that should not be called directly. It handles the non-ACL (chgrp+setgid) case of restrict_file and restrict_gid. @param target the target file @param st_mode the desired mode @param chown chowning function @param chmod chmodding function @param logger a logging.Logger for log messages @protected """ if logger is not None: logger.info('%s: chgrp to %s'%(str(target),self.__groupname)) chown(target,-1,self.__groupid) if stat.S_ISDIR(st_mode): smode = stat.S_ISGID | (stat.S_IMODE(st_mode)&okay_mode) if logger is not None: logger.info('%s: set mode on directory to 0%o' %(str(target),smode)) chmod( target, smode ) else: smode = stat.S_IMODE(st_mode)&okay_mode if logger is not None: logger.info('%s: set mode on file to 0%o'%(str(target),smode)) chmod( target, smode ) def acl_restrict_file(self,target,st_mode,set_acl,logger): """!Internal function that restricts files using ACLs This is an internal implementation function that should not be called directly. It handles the ACL case of restrict_file. @protected @param target the target file @param st_mode the desired access @param set_acl the acl-setting function @param logger a logging.Logger for log messages """ if stat.S_ISDIR(st_mode): if logger is not None: logger.info('%s: use acl to restrict dir to group %s' %(str(target),self.groupname)) set_acl(target,ACL_TYPE_ACCESS) set_acl(target,ACL_TYPE_DEFAULT) else: if logger is not None: logger.info('%s: use acl to restrict file to group %s' %(str(target),self.groupname)) set_acl(target,ACL_TYPE_ACCESS) def restrict_file(self,filename,st_mode=None,logger=None): """!Adds the requested restrictions to the specified file or directory. This routine needs to stat the opened file to get the stat.st_mode. @param st_mode To avoid a stat call, send st_mode into the optional argument. @param filename the target file @param logger a logging.Logger for log messages""" if st_mode is None: if logger is not None: logger.info(filename+': stat file') s=os.stat(filename) st_mode=s.st_mode if self.__use_acl: acl=self.acl_for(stat.S_IMODE(st_mode)) self.acl_restrict_file(filename,st_mode,acl.to_file,logger) else: self.chgrp_restrict(filename,st_mode,os.chown,os.chmod,logger) def restrict_fd(self,fd,st_mode=None,logger=None): """Adds the requested restrictions to an opened file. This routine needs to stat the opened file to get the stat.st_mode. @param st_mode To avoid a stat call, send st_mode into the optional argument. @param fd the target file descriptor @param logger a logging.Logger for log messages""" if hasattr(fd,'fileno'): fd=fd.fileno() if st_mode is None: if logger is not None: logger.info(str(fd)+': stat fileno') s=os.fstat(fd) st_mode=s.st_mode if self.__use_acl: acl=self.acl_for(stat.S_IMODE(st_mode)) if logger is not None: logger.info('%s: set acl of fileno to restrict to group %s' %(str(fd),self.__groupname)) acl.to_fd(fd) else: self.chgrp_restrict(fd,st_mode,os.fchown,os.fchmod,logger) ##@var rstprod_tagger #The RestrictionClass object used for tag_rstprod. Create this with #make_rstprod_tagger rstprod_tagger=None def make_rstprod_tagger(group='rstprod',use_acl=None,logger=None): """!Creates the rstprod_tagger object for use by tag_rstprod""" global rstprod_tagger rstprod_tagger=RestrictionClass(group,use_acl,logger) def tag_rstprod(target,logger=None): """!Places a file or directory under the rstprod restriction class. This command will attempt to raise RstprodForbidden if it is run on a cluster that is not supposed to have rstprod data (only GAEA, Zeus and WCOSS are allowed). This routine uses the approved rstprod protection mechanisms on each cluster: * Zeus --- place the file in the rstprod access control list, and make it unreadable to anyone else. * WCOSS --- place the file in group rstprod and remove permissions for others. * GAEA --- same as WCOSS Note that the NOAA Jet cluster is not allowed to contain restricted data, so this routine will raise RstprodForbidden on that cluster.""" if rstprod_tagger is None: make_rstprod_tagger(logger=logger) if isinstance(target,str): rstprod_tagger.restrict_file(target,logger=logger) elif isinstance(target,file) or isinstance(target,int): rstprod_tagger.restrict_fd(target,logger=logger) else: raise TypeError('The tag_rstprod target argument must be an int, a file ' 'or a basestring. You supplied a %s %s' %(type(target).__name__,repr(target)))