def _modify_namespace_users(self, add: bool, namespace_id, admin_user): """ :param add: True to add the user to the namespace, False to remove. """ not_none(namespace_id, 'namespace_id') not_none(admin_user, 'admin_user') op = '$addToSet' if add else '$pull' try: res = self._db[_COL_NAMESPACES].update_one( {_FLD_NS_ID: namespace_id.id}, { op: { _FLD_USERS: { _FLD_AUTHSOURCE: admin_user.authsource_id.id, _FLD_NAME: admin_user.username.name } } }) if res.matched_count != 1: raise NoSuchNamespaceError(namespace_id.id) if res.modified_count != 1: action = 'already administrates' if add else 'does not administrate' ex = UserExistsError if add else NoSuchUserError # might want diff exceps here raise ex('User {}/{} {} namespace {}'.format( admin_user.authsource_id.id, admin_user.username.name, action, namespace_id.id)) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def set_namespace_publicly_mappable(self, authsource_id: AuthsourceID, token: Token, namespace_id: NamespaceID, publicly_mappable: bool) -> None: """ Set a namespace to be publicly mappable, or remove that state. A publicly mappable namespace may have ID mappings added to it without the user being an administrator of the namespace. The user must always be an administrator of the primary ID of the ID tuple. :param authsource_id: The authentication source to be used to look up the user token. :param token: the user's token. :param namespace_id: the ID of the namespace to modify. :param publicly_mappable: True to set the namespace to publicly mappable, false otherwise. :raises TypeError: if authsource ID, namespace ID, or token are None. :raises NoSuchAuthsourceError: if there's no lookup handler for the provided authsource. :raises InvalidTokenError: if the token is invalid. :raises NoSuchNamespaceError: if the namespace does not exist. :raises UnauthorizedError: if the user is not authorized to administrate the namespace. """ not_none(token, 'token') not_none(namespace_id, 'namespace_id') user, _ = self._lookup.get_user(authsource_id, token) self._check_authed_for_ns_get(user, namespace_id) self._storage.set_namespace_publicly_mappable(namespace_id, publicly_mappable) _log('User %s/%s set namespace %s public map property to %s', user.authsource_id.id, user.username.name, namespace_id.id, publicly_mappable)
def _check_authsource_id(self, authsource_id: AuthsourceID) -> None: """ :raises NoSuchAuthsourceError: if there's no handler for the provided authsource. """ not_none(authsource_id, 'authsource_id') if authsource_id not in self._lookup: raise NoSuchAuthsourceError(authsource_id.id)
def get_namespace(self, namespace_id: NamespaceID, authsource_id: AuthsourceID = None, token: Token = None) -> Namespace: """ Get a namespace. If user credentials are provided and the user is a system admin or an admin of the namespace, the namespace user list will be returned. Otherwise, the user list will be empty. :param namespace_id: the ID of the namespace to get. :param authsource_id: the authsource of the provided token. :param token: the user's token. :raises TypeError: if the namespace ID is None or only one of the authsource ID or token are supplied. :raises NoSuchNamespaceError: if the namespace does not exist. :raises NoSuchAuthsourceError: if there's no lookup handler for the provided authsource. :raises InvalidTokenError: if the token is invalid. """ not_none(namespace_id, 'namespace_id') if bool(authsource_id) ^ bool(token): # xor raise TypeError( 'If token or authsource_id is specified, both must be specified' ) ns = self._storage.get_namespace(namespace_id) if token: authsource_id = cast( AuthsourceID, authsource_id) # mypy doesn't understand the xor user, admin = self._lookup.get_user(authsource_id, token) if admin or user in ns.authed_users: return ns return ns.without_users()
def get_mappings( self, oid: ObjectID, ns_filter: Iterable[NamespaceID] = None ) -> Tuple[Set[ObjectID], Set[ObjectID]]: """ Find mappings given a namespace / id combination. If the id does not exist, no results will be returned. :param oid: the namespace / id combination to match against. :param ns_filter: a list of namespaces with which to filter the results. Only results in these namespaces will be returned. :returns: a tuple of sets of object IDs. The first set in the tuple contains mappings where the provided object ID is the administrative object ID, and the second set contains the remainder of the mappings. :raise TypeError: if the object ID is None or the filter contains None. :raise NoSuchNamespaceError: if any of the namespaces do not exist. """ not_none(oid, 'oid') check = [oid.namespace_id] if ns_filter: no_Nones_in_iterable(ns_filter, 'ns_filter') check.extend(ns_filter) self._storage.get_namespaces(check) # check for existence return self._storage.find_mappings(oid, ns_filter=ns_filter)
def add_user_to_namespace(self, authsource_id: AuthsourceID, token: Token, namespace_id: NamespaceID, user: User) -> None: """ Add a user to a namespace. :param authsource_id: The authentication source to be used to look up the user token. :param token: the user's token. :param namespace_id: the namespace to modify. :param user: the user. :raises TypeError: if any of the arguments are None. :raises NoSuchAuthsourceError: if there's no handler for the provided authsource ID or the user's authsource. :raises NoSuchNamespaceError: if the namespace does not exist. :raises NoSuchUserError: if the user is invalid according to the appropriate user handler. :raises UserExistsError: if the user already administrates the namespace. :raises InvalidTokenError: if the token is invalid. :raises UnauthorizedError: if the user is not a system administrator. """ not_none(namespace_id, 'namespace_id') not_none(user, 'user') admin = self._check_sys_admin(authsource_id, token) self._check_valid_user(user) self._storage.add_user_to_namespace(namespace_id, user) _log('Admin %s/%s added user %s/%s to namespace %s', admin.authsource_id.id, admin.username.name, user.authsource_id.id, user.username.name, namespace_id.id)
def __init__(self, storage: IDMappingStorage) -> None: ''' Create a local user handler. :param storage: the storage system in which users are stored. ''' not_none(storage, 'storage') self._store = storage
def user_exists(self, username: Username) -> bool: not_none(username, 'username') try: return self._db[_COL_USERS].count_documents( {_FLD_USER: username.name}) == 1 except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def is_valid_user( self, username: Username) -> Tuple[bool, Optional[int], Optional[int]]: not_none(username, 'username') r = requests.get(self.auth_url + 'api/V2/users/?list=' + username.name, headers={'Authorization': self._token.token}) self._check_error(r) j = r.json() return (len(j) == 1, None, 3600)
def _check_valid_user(self, user): """ :raises NoSuchAuthsourceError: if there's no handler for the user's authsource. :raises NoSuchUserError: if the user is invalid according to the appropriate user handler. """ not_none(user, 'user') if not self._lookup.is_valid_user(user): raise NoSuchUserError('{}/{}'.format(user.authsource_id.id, user.username.name))
def set_user_as_admin(self, username: Username, admin: bool) -> None: """ Set or remove a local user's administration status. :param username: the name of the user to alter. :param admin: True to give the user admin privileges, False to remove them. If the user is already in the given state, no further action is taken. :raises TypeError: if the username is None. """ not_none(username, 'username') self._store.set_local_user_as_admin(username, admin)
def remove_mapping(self, primary_OID: ObjectID, secondary_OID: ObjectID) -> bool: not_none(primary_OID, 'primary_OID') not_none(secondary_OID, 'secondary_OID') try: res = self._db[_COL_MAPPINGS].delete_one( self.to_mapping_mongo_doc(primary_OID, secondary_OID)) return res.deleted_count == 1 except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def __init__(self, db: Database) -> None: """ Create a ID mapping storage system. :param db: the MongoDB database in which to store the mappings and other data. :raises StorageInitException: if the storage system could not be initialized properly. :raises TypeError: if the Mongo database is None. """ not_none(db, 'db') self._db = db self._ensure_indexes() self._check_schema() # MUST happen after ensuring indexes
def get_namespace(self, namespace_id: NamespaceID) -> Namespace: not_none(namespace_id, 'namespace_id') try: nsdoc = self._db[_COL_NAMESPACES].find_one( {_FLD_NS_ID: namespace_id.id}) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e if not nsdoc: raise NoSuchNamespaceError(namespace_id.id) return self._to_ns(nsdoc)
def add_mapping(self, primary_OID: ObjectID, secondary_OID: ObjectID) -> None: not_none(primary_OID, 'primary_OID') not_none(secondary_OID, 'secondary_OID') try: self._db[_COL_MAPPINGS].insert_one( self.to_mapping_mongo_doc(primary_OID, secondary_OID)) except DuplicateKeyError as e: pass # don't care, record is already there except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def get_user(self, token: HashedToken) -> Tuple[Username, bool]: not_none(token, 'token') try: userdoc = self._db[_COL_USERS].find_one( {_FLD_TOKEN: token.token_hash}, {_FLD_TOKEN: 0}) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e if not userdoc: raise InvalidTokenError() return (Username(userdoc[_FLD_USER]), userdoc[_FLD_ADMIN])
def create_user(self, username: Username) -> Token: ''' Create a new user in the local storage system. Returns a new token for that user. :param username: The name of the user to create. :raises TypeError: if the user name is None. :raises UserExistsError: if the user already exists. ''' not_none(username, 'username') t = tokens.generate_token() self._store.create_local_user(username, t.get_hashed_token()) return t
def new_token(self, username: Username) -> Token: ''' Generate a new token for a user in the local storage system. :param username: The name of the user to update. :raises TypeError: if the user name is None. :raises NoSuchUserError: if the user does not exist. ''' not_none(username, 'username') t = tokens.generate_token() self._store.update_local_user_token(username, t.get_hashed_token()) return t
def is_valid_user( self, username: Username) -> Tuple[bool, Optional[int], Optional[int]]: not_none(username, 'username') r = requests.get(self.auth_url + 'api/users/' + username.name + '.json') if 200 <= r.status_code <= 299: return (True, None, 3600) if r.status_code == 410: # Uses Gone to denote no such user return (False, None, 3600) raise IOError('Unexpected error from JGI server: ' + str(r.status_code))
def __init__(self, authsource_id: AuthsourceID, username: Username) -> None: """ Create a new user. :param authsource_id: The authentication source for the user. :param username: The name of the user. :raises TypeError: if any of the arguments are None. """ not_none(authsource_id, 'authsource_id') not_none(username, 'username') self.authsource_id = authsource_id self.username = username
def create_namespace(self, namespace_id: NamespaceID) -> None: not_none(namespace_id, 'namespace_id') try: self._db[_COL_NAMESPACES].insert_one({ _FLD_NS_ID: namespace_id.id, _FLD_PUB_MAP: False, _FLD_USERS: [] }) except DuplicateKeyError as e: raise NamespaceExistsError(namespace_id.id) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def set_local_user_as_admin(self, username: Username, admin: bool) -> None: not_none(username, 'username') admin = True if admin else False # more readable than admin and True try: res = self._db[_COL_USERS].update_one( {_FLD_USER: username.name}, {'$set': { _FLD_ADMIN: admin }}) if res.matched_count != 1: # don't care if user was updated or not, just found raise NoSuchUserError(username.name) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def set_namespace_publicly_mappable(self, namespace_id: NamespaceID, publicly_mappable: bool) -> None: not_none(namespace_id, 'namespace_id') pm = True if publicly_mappable else False # more readable than 'and True' try: res = self._db[_COL_NAMESPACES].update_one( {_FLD_NS_ID: namespace_id.id}, {'$set': { _FLD_PUB_MAP: pm }}) if res.matched_count != 1: # don't care if modified or not raise NoSuchNamespaceError(namespace_id.id) except PyMongoError as e: raise IDMappingStorageError('Connection to database failed: ' + str(e)) from e
def __init__(self, namespace_id: NamespaceID, data_id: str) -> None: """ Create a new object ID. :param namespace_id: The ID of the namespace in which the data ID resides. :param data_id: The ID of the data unit, no more than 1000 characters. :raises TypeError: if the namespace ID is None. :raises MissingParameterError: if the data ID is None or whitespace only. :raises IllegalParameterError: if the data ID does not meet the requirements. """ not_none(namespace_id, 'namespace_id') check_string(data_id, 'data id', max_len=1000) # should maybe check for control chars self.namespace_id = namespace_id self.id = data_id
def get_user( self, token: Token) -> Tuple[User, bool, Optional[int], Optional[int]]: not_none(token, 'token') r = requests.get(self.auth_url + 'api/sessions/' + token.token + '.json') if 400 <= r.status_code <= 499: raise InvalidTokenError( 'JGI auth server reported token is invalid: ' + str(r.status_code)) if 500 <= r.status_code <= 599: raise IOError('JGI auth server reported an internal error: ' + str(r.status_code)) sres = r.json() return (User(self._JGI, Username(str(sres['user']['id']))), False, None, 300)
def get_user( self, token: Token) -> Tuple[User, bool, Optional[int], Optional[int]]: not_none(token, 'token') r = requests.get(self.auth_url + 'api/V2/token', headers={'Authorization': token.token}) self._check_error(r) tokenres = r.json() r = requests.get(self.auth_url + 'api/V2/me', headers={'Authorization': token.token}) self._check_error(r) mres = r.json() return (User(self._KBASE, Username(tokenres['user'])), self._kbase_system_admin in mres['customroles'], tokenres['expires'] // 1000, tokenres['cachefor'] // 1000)
def __init__(self, namespace_id: NamespaceID, is_publicly_mappable: bool, authed_users: Set[User] = None) -> None: ''' Create a namespace. :param namespace_id: the ID of the namespace. :param is_publicly_mappable: whether the namespace is publicly mappable or not. :param authed_users: users that are authorized to administer the namespace. :raises TypeError: if namespace_id is None or authed_users contains None ''' not_none(namespace_id, 'namespace_id') self.namespace_id = namespace_id self.is_publicly_mappable = is_publicly_mappable self.authed_users = frozenset( authed_users) if authed_users else frozenset() no_Nones_in_iterable(self.authed_users, 'authed_users')
def _check_sys_admin(self, authsource_id: AuthsourceID, token: Token) -> User: """ :raises NoSuchAuthsourceError: if there's no handler for the provided authsource. :raises InvalidTokenError: if the token is invalid. :raises UnauthorizedError: if the user is not a system administrator. """ not_none(token, 'token') if authsource_id not in self._admin_authsources: raise UnauthorizedError( ('Auth source {} is not configured as a provider of ' + 'system administration status').format(authsource_id.id)) user, admin = self._lookup.get_user(authsource_id, token) if not admin: raise UnauthorizedError( 'User {}/{} is not a system administrator'.format( user.authsource_id.id, user.username.name)) return user
def __init__(self, user_lookup: UserLookupSet, admin_authsources: Set[AuthsourceID], storage: IDMappingStorage) -> None: """ Create the mapper. :param user_lookup: the set of user lookup handlers to query when looking up user names from tokens or checking that a provided user name is valid. :param admin_authsources: the set of auth sources that are valid system admin sources. The admin state returned by other auth sources will be ignored. :param storage: the mapping storage system. """ not_none(user_lookup, 'user_lookup') no_Nones_in_iterable(admin_authsources, 'admin_authsources') not_none(storage, 'storage') self._storage = storage self._lookup = user_lookup self._admin_authsources = admin_authsources
def create_namespace(self, authsource_id: AuthsourceID, token: Token, namespace_id: NamespaceID) -> None: """ Create a namespace. :param authsource_id: The authentication source to be used to look up the user token. :param token: the user's token. :param namespace_id: The namespace to create. :raises TypeError: if any of the arguments are None. :raises NoSuchAuthsourceError: if there's no handler for the provided authsource. :raises NamespaceExistsError: if the namespace already exists. :raises InvalidTokenError: if the token is invalid. :raises UnauthorizedError: if the user is not a system administrator. """ not_none(namespace_id, 'namespace_id') admin = self._check_sys_admin(authsource_id, token) self._storage.create_namespace(namespace_id) _log('Admin %s/%s created namespace %s', admin.authsource_id.id, admin.username.name, namespace_id.id)