async def send(conn, batch_list, timeout, webhook=False): """Send batch_list to sawtooth.""" batch_request = client_batch_submit_pb2.ClientBatchSubmitRequest() batch_request.batches.extend(list(batch_list.batches)) validator_response = await conn.send( validator_pb2.Message.CLIENT_BATCH_SUBMIT_REQUEST, batch_request.SerializeToString(), timeout, ) client_response = client_batch_submit_pb2.ClientBatchSubmitResponse() client_response.ParseFromString(validator_response.content) status = client_response.status if not webhook: if status == client_batch_submit_pb2.ClientBatchSubmitResponse.INTERNAL_ERROR: raise ApiInternalError("Internal Error") elif status == client_batch_submit_pb2.ClientBatchSubmitResponse.INVALID_BATCH: raise ApiBadRequest("Invalid Batch") elif status == client_batch_submit_pb2.ClientBatchSubmitResponse.QUEUE_FULL: raise ApiInternalError("Queue Full") elif status != client_batch_submit_pb2.ClientBatchSubmitResponse.OK: return None status_request = client_batch_submit_pb2.ClientBatchStatusRequest() status_request.batch_ids.extend( list(b.header_signature for b in batch_list.batches)) status_request.wait = True status_request.timeout = timeout validator_response = await conn.send( validator_pb2.Message.CLIENT_BATCH_STATUS_REQUEST, status_request.SerializeToString(), timeout, ) status_response = client_batch_submit_pb2.ClientBatchStatusResponse() status_response.ParseFromString(validator_response.content) status = status_response.status if not webhook: if status != client_batch_submit_pb2.ClientBatchStatusResponse.OK: raise ApiInternalError("Internal Error") elif status != client_batch_submit_pb2.ClientBatchStatusResponse.OK: return None response = status_response.batch_statuses[0] status = response.status if not webhook: if status == client_batch_submit_pb2.ClientBatchStatus.INVALID: raise ApiBadRequest("Bad Request: {}".format( response.invalid_transactions[0].message)) elif status == client_batch_submit_pb2.ClientBatchStatus.PENDING: raise ApiInternalError("Internal Error: Transaction timed out.") elif status == client_batch_submit_pb2.ClientBatchStatus.UNKNOWN: raise ApiInternalError("Internal Error: Unspecified error.") return status
async def send(conn, batch_list, timeout): batch_request = client_batch_submit_pb2.ClientBatchSubmitRequest() batch_request.batches.extend(list(batch_list.batches)) validator_response = await conn.send( validator_pb2.Message.CLIENT_BATCH_SUBMIT_REQUEST, batch_request.SerializeToString(), timeout, ) client_response = client_batch_submit_pb2.ClientBatchSubmitResponse() client_response.ParseFromString(validator_response.content) if (client_response.status == client_batch_submit_pb2.ClientBatchSubmitResponse.INTERNAL_ERROR): raise ApiInternalError("Internal Error") elif (client_response.status == client_batch_submit_pb2.ClientBatchSubmitResponse.INVALID_BATCH): raise ApiBadRequest("Invalid Batch") elif (client_response.status == client_batch_submit_pb2.ClientBatchSubmitResponse.QUEUE_FULL): raise ApiInternalError("Queue Full") status_request = client_batch_submit_pb2.ClientBatchStatusRequest() status_request.batch_ids.extend( list(b.header_signature for b in batch_list.batches)) status_request.wait = True status_request.timeout = timeout validator_response = await conn.send( validator_pb2.Message.CLIENT_BATCH_STATUS_REQUEST, status_request.SerializeToString(), timeout, ) status_response = client_batch_submit_pb2.ClientBatchStatusResponse() status_response.ParseFromString(validator_response.content) if status_response.status != client_batch_submit_pb2.ClientBatchStatusResponse.OK: raise ApiInternalError("Internal Error") resp = status_response.batch_statuses[0] if resp.status == client_batch_submit_pb2.ClientBatchStatus.COMMITTED: return resp elif resp.status == client_batch_submit_pb2.ClientBatchStatus.INVALID: raise ApiBadRequest("Bad Request: {}".format( resp.invalid_transactions[0].message)) elif resp.status == client_batch_submit_pb2.ClientBatchStatus.PENDING: raise ApiInternalError( "Internal Error: Transaction submitted but timed out.") elif resp.status == client_batch_submit_pb2.ClientBatchStatus.UNKNOWN: raise ApiInternalError( "Internal Error: Something went wrong. Try again later.")
async def fetch_latest_block(conn): try: return (await r.table("blocks").max(index="block_num").merge({ "id": r.row["block_id"], "num": r.row["block_num"] }).without("block_id", "block_num").run(conn)) except ReqlNonExistenceError: raise ApiInternalError("Internal Error: No block data found in state")
async def create_new_role(request): """Create a new role.""" log_request(request) env = Env() if not env.int("ENABLE_NEXT_BASE_USE"): raise ApiDisabled("Not a valid action. Source not enabled.") required_fields = ["name", "administrators", "owners"] validate_fields(required_fields, request.json) role_title = " ".join(request.json.get("name").split()) conn = await create_connection() response = await roles_query.roles_search_duplicate(conn, role_title) conn.close() if not response: txn_key, txn_user_id = await get_transactor_key(request) role_id = str(uuid4()) if request.json.get("metadata") is None: set_metadata = {} else: set_metadata = request.json.get("metadata") set_metadata["sync_direction"] = "OUTBOUND" batch_list = Role().batch_list( signer_keypair=txn_key, signer_user_id=txn_user_id, name=role_title, role_id=role_id, metadata=set_metadata, admins=request.json.get("administrators"), owners=request.json.get("owners"), description=request.json.get("description"), ) sawtooth_response = await send( request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT ) if not sawtooth_response: LOGGER.warning("There was an error submitting the sawtooth transaction.") return await handle_errors( request, ApiInternalError( "There was an error submitting the sawtooth transaction." ), ) return create_role_response(request, role_id) return await handle_errors( request, ApiTargetConflict( "Error: Could not create this role because the role name already exists." ), )
async def fetch_latest_block_with_retry(conn, tries=5): attempts = tries while attempts > 0: try: return await fetch_latest_block(conn) except ReqlQueryLogicError: LOGGER.debug("Query attempt failed") sleep_time = (tries - attempts + 1) * 2 LOGGER.debug("Retrying in %s secs", sleep_time) time.sleep(sleep_time) attempts -= 1 raise ApiInternalError("Internal Error: No block data found in state")
async def check_admin_status(next_id): """Verfiy that a user is a member of NEXT admins. Return boolean. Args: next_id: str: user's next_id """ conn = await create_connection() admin_role = await get_role_by_name(conn, "NextAdmins") if not admin_role: raise ApiInternalError( "NEXT administrator group has not been created.") admin_membership = await get_role_membership(conn, next_id, admin_role[0]["role_id"]) conn.close() if admin_membership: return True return False
async def create_new_user(request): """Create a new user. Must be an adminsitrator. Args: request: obj: incoming request object """ log_request(request, True) # Validate that we have all fields required_fields = ["name", "username", "password", "email"] validate_fields(required_fields, request.json) # Check if username already exists conn = await create_connection() username = request.json.get("username") if await users_query.fetch_username_match_count(conn, username) > 0: # Throw Error response to Next_UI return await handle_errors( request, ApiTargetConflict("Username already exists.")) conn.close() # Check to see if they are trying to create the NEXT admin env = Env() next_admin = { "name": env("NEXT_ADMIN_NAME"), "username": env("NEXT_ADMIN_USER"), "email": env("NEXT_ADMIN_EMAIL"), "password": env("NEXT_ADMIN_PASS"), } if request.json != next_admin: # Try to see if they are in NEXT if not env.int("ENABLE_NEXT_BASE_USE"): raise ApiDisabled("Not a valid action. Source not enabled.") txn_key, txn_user_id, next_id, key_pair = await non_admin_creation( request) else: txn_key, txn_user_id, next_id, key_pair = await next_admin_creation( request) if request.json.get("metadata") is None: set_metadata = {} else: set_metadata = request.json.get("metadata") set_metadata["sync_direction"] = "OUTBOUND" # Build create user transaction batch_list = User().batch_list( signer_keypair=txn_key, signer_user_id=txn_user_id, next_id=next_id, name=request.json.get("name"), username=request.json.get("username"), email=request.json.get("email"), metadata=set_metadata, manager_id=request.json.get("manager"), key=key_pair.public_key, ) # Submit transaction and wait for complete sawtooth_response = await send(request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT) if not sawtooth_response: return await handle_errors( request, ApiInternalError( "There was an error submitting the sawtooth transaction."), ) # Save new user in auth table salt = hashlib.sha256(os.urandom(60)).hexdigest().encode("utf-8") password = request.json.get("password").encode("utf-8") hashed_password = hashlib.pbkdf2_hmac("sha256", password, salt, 100000).hex() encrypted_private_key = encrypt_private_key(AES_KEY, key_pair.public_key, key_pair.private_key_bytes) auth_entry = { "next_id": next_id, "salt": salt, "hashed_password": hashed_password, "encrypted_private_key": encrypted_private_key, "username": request.json.get("username"), "email": request.json.get("email"), } mapping_data = { "next_id": next_id, "provider_id": "NEXT-created", "remote_id": None, "public_key": key_pair.public_key, "encrypted_key": encrypted_private_key, "active": True, } # Insert to user_mapping and close await auth_query.create_auth_entry(auth_entry) conn = await create_connection() await users_query.create_user_map_entry(conn, mapping_data) conn.close() # Send back success response return json({"data": {"user": {"id": next_id}}})
async def create_new_user(request): """Create a new user.""" env = Env() admin_role = { "name": env("NEXT_ADMIN_NAME"), "username": env("NEXT_ADMIN_USER"), "email": env("NEXT_ADMIN_EMAIL"), "password": env("NEXT_ADMIN_PASS"), } if request.json != admin_role: if not env.int("ENABLE_NEXT_BASE_USE"): raise ApiBadRequest("Not a valid action. Source not enabled") required_fields = ["name", "username", "password", "email"] utils.validate_fields(required_fields, request.json) username_created = request.json.get("username") # Check if username already exists if (await users_query.fetch_username_match_count( request.app.config.DB_CONN, username_created) > 0): # Throw Error response to Next_UI return await handle_errors( request, ApiTargetConflict( "Username already exists. Please give a different Username."), ) # Generate keys key_pair = Key() next_id = str(uuid4()) encrypted_private_key = encrypt_private_key(AES_KEY, key_pair.public_key, key_pair.private_key_bytes) if request.json.get("metadata") is None or request.json.get( "metadata") == {}: set_metadata = {} else: set_metadata = request.json.get("metadata") set_metadata["sync_direction"] = "OUTBOUND" # Build create user transaction batch_list = User().batch_list( signer_keypair=key_pair, signer_user_id=next_id, next_id=next_id, name=request.json.get("name"), username=request.json.get("username"), email=request.json.get("email"), metadata=set_metadata, manager_id=request.json.get("manager"), key=key_pair.public_key, ) # Submit transaction and wait for complete sawtooth_response = await utils.send(request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT) if not sawtooth_response: LOGGER.warning( "There was an error submitting the sawtooth transaction.") return await handle_errors( request, ApiInternalError( "There was an error submitting the sawtooth transaction."), ) # Save new user in auth table hashed_password = hashlib.sha256( request.json.get("password").encode("utf-8")).hexdigest() auth_entry = { "next_id": next_id, "hashed_password": hashed_password, "encrypted_private_key": encrypted_private_key, "username": request.json.get("username"), "email": request.json.get("email"), } mapping_data = { "next_id": next_id, "provider_id": "NEXT-created", "remote_id": None, "public_key": key_pair.public_key, "encrypted_key": encrypted_private_key, "active": True, } # Insert to user_mapping and close await auth_query.create_auth_entry(auth_entry) await users_query.create_user_map_entry(request.app.config.DB_CONN, mapping_data) # Send back success response return create_user_response(request, next_id)
async def create_new_role(request): """Create a new role.""" required_fields = ["name", "administrators", "owners"] utils.validate_fields(required_fields, request.json) role_title = " ".join(request.json.get("name").split()) response = await roles_query.roles_search_duplicate( request.app.config.DB_CONN, role_title ) if request.json.get("metadata") is None or request.json.get("metadata") == {}: set_metadata = {} else: set_metadata = request.json.get("metadata") set_metadata["sync_direction"] = "OUTBOUND" if not response: txn_key, txn_user_id = await utils.get_transactor_key(request) role_id = str(uuid4()) batch_list = Role().batch_list( signer_keypair=txn_key, signer_user_id=txn_user_id, name=role_title, role_id=role_id, metadata=set_metadata, admins=request.json.get("administrators"), owners=request.json.get("owners"), description=request.json.get("description"), ) sawtooth_response = await utils.send( request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT ) if not sawtooth_response: LOGGER.warning("There was an error submitting the sawtooth transaction.") return await handle_errors( request, ApiInternalError( "There was an error submitting the sawtooth transaction" ), ) if role_title != "NextAdmins": distinguished_name_formatted = "CN=" + role_title + "," + GROUP_BASE_DN data_formatted = { "created_date": r.now(), "distinguished_name": distinguished_name_formatted, "group_nickname": role_title, "group_types": -2147483646, "name": role_title, "remote_id": distinguished_name_formatted, } env = Env() if env.int("ENABLE_LDAP_SYNC", 0): provider = env("LDAP_DC") elif env.int("ENABLE_AZURE_SYNC", 0): provider = env("TENANT_ID") else: provider = "NEXT-created" outbound_entry = { "data": data_formatted, "data_type": "group", "timestamp": r.now(), "provider_id": provider, "status": "UNCONFIRMED", } # Insert to outbound_queue and close await roles_query.insert_to_outboundqueue( request.app.config.DB_CONN, outbound_entry ) else: LOGGER.info( "The role being created is NextAdmins, which is local to NEXT and will not be inserted into the outbound_queue." ) return create_role_response(request, role_id) return await handle_errors( request, ApiTargetConflict( "Error: Could not create this role because the role name already exists." ), )
async def add_role_member(request, role_id): """Add a member to a role.""" required_fields = ["id"] utils.validate_fields(required_fields, request.json) txn_key, txn_user_id = await utils.get_transactor_key(request) proposal_id = str(uuid4()) approver = await fetch_relationships("role_owners", "role_id", role_id).run( request.app.config.DB_CONN ) batch_list = Role().member.propose.batch_list( signer_keypair=txn_key, signer_user_id=txn_user_id, proposal_id=proposal_id, role_id=role_id, pack_id=request.json.get("pack_id"), next_id=request.json.get("id"), reason=request.json.get("reason"), metadata=request.json.get("metadata"), assigned_approver=approver, ) batch_status = await utils.send( request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT, request.json.get("tracker") and True, ) role_resource = await roles_query.fetch_role_resource( request.app.config.DB_CONN, role_id ) owners = role_resource.get("owners") requester_id = request.json.get("id") if requester_id in owners: is_proposal_ready = await wait_for_resource_in_db( "proposals", "proposal_id", proposal_id, max_attempts=30 ) if not is_proposal_ready: LOGGER.warning( "Max attempts exceeded. Proposal %s not found in RethinkDB.", proposal_id, ) return await handle_errors( request, ApiInternalError( "Max attempts exceeded. Proposal %s not found in RethinkDB." % proposal_id ), ) request.json["status"] = "APPROVED" request.json["reason"] = "I am the owner of this role" await proposals.update_proposal(request, proposal_id) if request.json.get("tracker"): events = {"batch_status": batch_status, "member_status": "MEMBER"} return utils.create_tracker_response(events) return json( { "message": "Owner is the requester. Proposal is autoapproved", "proposal_id": proposal_id, } ) if request.json.get("tracker"): events = {"batch_status": batch_status} if batch_status == 1: events["member_status"] = "PENDING" return utils.create_tracker_response(events) return json({"proposal_id": proposal_id})
async def delete_role(request, role_id): """Delete a role by it's next_id. Args: role_id: str: the role_id field of the targeted role Returns: json: dict: { message: str: the status of the role delete operation deleted: int: count of the number of roles that were deleted } Raises: ApiForbidden: The user is not a system admin or owner of the targeted role. ApiNotFound: The role does not exist in RethinkDB. ApiInternalError: There was an error compiling blockchain transactions. """ txn_key, txn_user_id = await utils.get_transactor_key(request) # does the role exist? if not await roles_query.does_role_exist(request.app.config.DB_CONN, role_id): LOGGER.warning( "Nonexistent Role – User %s is attempting to delete the nonexistent role %s", txn_user_id, role_id, ) return await handle_not_found( request, ApiNotFound("The targeted role does not exist.") ) is_role_owner = await check_role_owner_status(txn_user_id, role_id) if not is_role_owner: is_admin = await check_admin_status(txn_user_id) if not is_admin: LOGGER.warning( "Permission Denied – User %s does not have sufficient privilege to delete role %s.", txn_user_id, role_id, ) return await handle_errors( request, ApiForbidden("You do not have permission to delete this role.") ) txn_list = [] txn_list = await create_rjct_ppsls_role_txns( txn_key, role_id, txn_user_id, txn_list ) txn_list = await create_del_admin_by_role_txns(txn_key, role_id, txn_list) txn_list = await create_del_mmbr_by_role_txns(txn_key, role_id, txn_list) txn_list = await create_del_ownr_by_role_txns(txn_key, role_id, txn_list) txn_list = create_del_role_txns(txn_key, role_id, txn_list) # validate transaction list if not txn_list: LOGGER.warning( "txn_list is empty. There was an error processing the delete role transactions. Transaction list: %s", txn_list, ) return await handle_errors( request, ApiInternalError( "An error occurred while creating the blockchain transactions to delete the role." ), ) batch = batcher.make_batch_from_txns(transactions=txn_list, signer_keypair=txn_key) batch_list = batcher.batch_to_list(batch=batch) await utils.send( request.app.config.VAL_CONN, batch_list, request.app.config.TIMEOUT ) return json( {"message": "Role {} successfully deleted".format(role_id), "deleted": 1} )