def create_launcher_api(service: LauncherService, config: Config) -> Blueprint: bp = Blueprint( "create_launcher_api", __name__, ) auth = Auth(config) @bp.route("/launcher/run/<string:study_id>", methods=["POST"]) @auth.protected() def run(study_id: str) -> Any: """ Run study --- responses: '200': content: application/json: schema: $ref: '#/definitions/RunInfo' description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized parameters: - in: path name: study_id required: true description: study id schema: type: string definitions: - schema: id: RunInfo properties: job_id: type: string tags: - Run Studies """ params = RequestParameters(user=Auth.get_current_user()) return jsonify({"job_id": service.run_study(study_id, params)}) @bp.route("/launcher/jobs", methods=["GET"]) @auth.protected() def get_job() -> Any: """ Retrieve jobs --- responses: '200': content: application/json: schema: type: array items: $ref: '#/definitions/LaunchJob' description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized parameters: - in: query name: study required: false description: study id schema: type: string definitions: - schema: id: LaunchJob properties: id: type: string study_id: type: string job_status: type: string creation_date: type: string completion_date: type: string msg: type: string exit_code: type: number tags: - Run Studies """ study_id: Optional[str] = None if "study" in request.args: study_id = request.args["study"] return jsonify([job.to_dict() for job in service.get_jobs(study_id)]) @bp.route("/launcher/jobs/<uuid:job_id>", methods=["GET"]) @auth.protected() def get_result(job_id: UUID) -> Any: """ Retrieve job info from job id --- responses: '200': content: application/json: schema: $ref: '#/definitions/LaunchJob' description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized parameters: - in: path name: job_id required: true description: job id schema: type: string definitions: - schema: id: LaunchJob properties: id: type: string study_id: type: string job_status: type: string creation_date: type: string completion_date: type: string msg: type: string exit_code: type: number tags: - Run Studies """ return jsonify(service.get_result(job_id).to_dict()) return bp
def create_login_api(service: LoginService, config: Config, jwt: JWTManager) -> Blueprint: bp = Blueprint( "create_login_api", __name__, template_folder=str(config.resources_path / "templates"), ) auth = Auth(config) def generate_tokens(user: JWTUser, expire: Optional[timedelta] = None) -> Any: access_token = create_access_token(identity=user.to_dict(), expires_delta=expire) refresh_token = create_refresh_token(identity=user.to_dict()) return { "user": user.id, "access_token": access_token, "refresh_token": refresh_token, } @jwt.token_in_blocklist_loader # type: ignore def check_if_token_is_revoked(jwt_header: Any, jwt_payload: JSON) -> bool: id = jwt_payload["sub"]["id"] type = jwt_payload["sub"]["type"] return type == "bots" and not service.exists_bot(id) @bp.route("/login", methods=["POST"]) def login() -> Any: """ Login --- responses: '200': content: application/json: schema: $ref: '#/definitions/UserCredentials' description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized consumes: - application/x-www-form-urlencoded parameters: - in: body name: body required: true description: user credentials schema: id: User required: - username - password properties: username: type: string password: type: string definitions: - schema: id: UserCredentials properties: user: type: string description: User name access_token: type: string refresh_token: type: string tags: - User """ username = request.form.get("username") or request.json.get("username") password = request.form.get("password") or request.json.get("password") if not username: return jsonify({"msg": "Missing username parameter"}), 400 if not password: return jsonify({"msg": "Missing password parameter"}), 400 user = service.authenticate(username, password) if not user: return jsonify({"msg": "Bad username or password"}), 401 # Identity can be any data that is json serializable resp = generate_tokens(user) return ( jsonify(resp), 200, ) @bp.route("/refresh", methods=["POST"]) @jwt_required(refresh=True) # type: ignore def refresh() -> Any: """ Refresh access token --- responses: '200': content: application/json: schema: $ref: '#/definitions/UserCredentials' description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized consumes: - application/x-www-form-urlencoded parameters: - in: header name: Authorization required: true description: refresh token schema: type: string description: (Bearer {token}) Refresh token received from login or previous refreshes definitions: - schema: id: UserCredentials properties: user: type: string description: User name access_token: type: string refresh_token: type: string tags: - User """ identity = get_jwt_identity() user = service.get_jwt(identity["id"]) if user: resp = generate_tokens(user) return ( jsonify(resp), 200, ) else: return "Token invalid", 403 @bp.route("/users", methods=["GET"]) @auth.protected() def users_get_all() -> Any: params = RequestParameters(user=Auth.get_current_user()) return jsonify([u.to_dict() for u in service.get_all_users(params)]) @bp.route("/users/<int:id>", methods=["GET"]) @auth.protected() def users_get_id(id: int) -> Any: params = RequestParameters(user=Auth.get_current_user()) u = service.get_user(id, params) if u: return jsonify(u.to_dict()) else: return "", 404 @bp.route("/users", methods=["POST"]) @auth.protected() def users_create() -> Any: params = RequestParameters(user=Auth.get_current_user()) create_user = UserCreateDTO.from_dict(json.loads(request.data)) return jsonify(service.create_user(create_user, params).to_dict()) @bp.route("/users/<int:id>", methods=["PUT"]) @auth.protected() def users_update(id: int) -> Any: params = RequestParameters(user=Auth.get_current_user()) u = User.from_dict(json.loads(request.data)) if id != u.id: return "Id in path must be same id in body", 400 return jsonify(service.save_user(u, params).to_dict()) @bp.route("/users/<int:id>", methods=["DELETE"]) @auth.protected() def users_delete(id: int) -> Any: params = RequestParameters(user=Auth.get_current_user()) service.delete_user(id, params) return jsonify(id), 200 @bp.route("/groups", methods=["GET"]) @auth.protected() def groups_get_all() -> Any: params = RequestParameters(user=Auth.get_current_user()) return jsonify([g.to_dict() for g in service.get_all_groups(params)]) @bp.route("/groups/<int:id>", methods=["GET"]) @auth.protected() def groups_get_id(id: str) -> Any: params = RequestParameters(user=Auth.get_current_user()) group = service.get_group(id, params) if group: return jsonify(group.to_dict()) else: return f"Group {id} not found", 404 @bp.route("/groups", methods=["POST"]) @auth.protected() def groups_create() -> Any: params = RequestParameters(user=Auth.get_current_user()) group = Group.from_dict(json.loads(request.data)) return jsonify(service.save_group(group, params).to_dict()) @bp.route("/groups/<int:id>", methods=["DELETE"]) @auth.protected() def groups_delete(id: str) -> Any: params = RequestParameters(user=Auth.get_current_user()) service.delete_group(id, params) return jsonify(id), 200 @bp.route("/roles/group/<string:group>", methods=["GET"]) @auth.protected() def roles_get_all(group: str) -> Any: params = RequestParameters(user=Auth.get_current_user()) return jsonify([ r.to_dict() for r in service.get_all_roles_in_group(group=group, params=params) ]) @bp.route("/roles", methods=["POST"]) @auth.protected() def role_create() -> Any: params = RequestParameters(user=Auth.get_current_user()) role = RoleCreationDTO.from_dict(json.loads(request.data)) return jsonify(service.save_role(role, params).to_dict()) @bp.route("/roles/<string:group>/<int:user>", methods=["DELETE"]) @auth.protected() def roles_delete(user: int, group: str) -> Any: params = RequestParameters(user=Auth.get_current_user()) service.delete_role(user, group, params) return jsonify((user, group)), 200 @bp.route("/bots", methods=["POST"]) @auth.protected() def bots_create() -> Any: """ Create Bot --- responses: '200': content: application/json: schema: type: string description: Bot token API description: Successful operation '400': description: Invalid request '401': description: Unauthenticated User '403': description: Unauthorized consumes: - application/json parameters: - in: body name: body required: true description: Bot schema: id: User required: - name - group - role properties: name: type: string description: Bot name isAuthor: type: boolean description: Set Bot impersonator between itself or it owner group: type: string description: group id linked to bot role: type: int description: RoleType used by bot. Should be lower or equals ot owner role type inside same group tags: - Bot """ params = RequestParameters(user=Auth.get_current_user()) create = BotCreateDTO.from_dict(json.loads(request.data)) bot = service.save_bot(create, params) if not bot: return UserHasNotPermissionError() group = service.get_group(create.group, params) if not group: return UserHasNotPermissionError() jwt = JWTUser( id=bot.id, impersonator=bot.get_impersonator(), type=bot.type, groups=[JWTGroup(id=group.id, name=group.name, role=create.role)], ) tokens = generate_tokens(jwt, expire=timedelta(days=368 * 200)) return tokens["access_token"] @bp.route("/bots/<int:id>", methods=["GET"]) @auth.protected() def get_bot(id: int) -> Any: params = RequestParameters(user=Auth.get_current_user()) bot = service.get_bot(id, params) return jsonify(bot.to_dict()), 200 @bp.route("/bots", methods=["GET"]) @auth.protected() def get_all_bots() -> Any: params = RequestParameters(user=Auth.get_current_user()) owner = request.args.get("owner", default=None, type=int) bots = (service.get_all_bots_by_owner(owner, params) if owner else service.get_all_bots(params)) return jsonify([b.to_dict() for b in bots]), 200 @bp.route("/bots/<int:id>", methods=["DELETE"]) @auth.protected() def bots_delete(id: int) -> Any: """ Revoke bot --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: id required: true description: bot id schema: type: int tags: - Bot """ params = RequestParameters(user=Auth.get_current_user()) service.delete_bot(id, params) return jsonify(id), 200 @bp.route("/protected") @auth.protected() def protected() -> Any: return f"user id={get_jwt_identity()}" @bp.route("/auth") @auth.protected() def auth_needed() -> Any: return "ok" return bp
def create_study_routes(storage_service: StorageService, config: Config) -> Blueprint: bp = Blueprint("create_study_route", __name__) auth = Auth(config) @bp.route("/studies", methods=["GET"]) @auth.protected() def get_studies() -> Any: """ Get Studies --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request tags: - Manage Studies """ params = RequestParameters(user=Auth.get_current_user()) available_studies = storage_service.get_studies_information(params) return jsonify(available_studies), HTTPStatus.OK.value @bp.route("/studies", methods=["POST"]) @auth.protected() def import_study() -> Any: """ Import Study --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: query name: groups required: false description: list of group id assignment separated by comma schema: type: string tags: - Manage Studies """ if "study" not in request.files: content = "No data provided." code = HTTPStatus.BAD_REQUEST.value return content, code zip_binary = io.BytesIO(request.files["study"].read()) params = RequestParameters(user=Auth.get_current_user()) groups_arg = request.args.get("groups") group_ids = groups_arg.split(",") if groups_arg is not None else [] uuid = storage_service.import_study(zip_binary, group_ids, params) content = "/studies/" + uuid code = HTTPStatus.CREATED.value return jsonify(content), code @bp.route( "/studies/<path:path>", methods=["GET"], ) @auth.protected() def get_study(path: str) -> Any: """ Read data --- responses: '200': description: Successful operation content: application/json: {} '404': description: File not found parameters: - in: path name: uuid required: true schema: type: string - in: path name: path schema: type: string required: true - in: query name: depth schema: type: string tags: - Manage Data inside Study """ parameters = RequestParameters(user=Auth.get_current_user()) depth = request.args.get("depth", 3, type=int) output = storage_service.get(path, depth, parameters) return jsonify(output), 200 @bp.route( "/studies/<string:uuid>/copy", methods=["POST"], ) @auth.protected() def copy_study(uuid: str) -> Any: """ Copy study --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid stored in server schema: type: string - in: query name: dest required: true description: new study name schema: type: string - in: query name: groups required: false description: list of group id assignment separated by comma schema: type: string tags: - Manage Studies """ source_uuid = uuid destination_study_name = request.args.get("dest") groups_arg = request.args.get("groups") group_ids = groups_arg.split(",") if groups_arg is not None else [] if destination_study_name is None: content = "Copy operation need a dest query parameter." code = HTTPStatus.BAD_REQUEST.value return content, code source_uuid_sanitized = sanitize_uuid(source_uuid) destination_name_sanitized = sanitize_study_name( destination_study_name) params = RequestParameters(user=Auth.get_current_user()) destination_uuid = storage_service.copy_study( src_uuid=source_uuid_sanitized, dest_study_name=destination_name_sanitized, group_ids=group_ids, params=params, ) code = HTTPStatus.CREATED.value return destination_uuid, code @bp.route( "/studies/<string:name>", methods=["POST"], ) @auth.protected() def create_study(name: str) -> Any: """ Create study name --- description: Create an empty study responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: name required: true description: study name asked schema: type: string - in: query name: groups required: false description: list of group id assignment separated by comma schema: type: string tags: - Manage Studies """ name_sanitized = sanitize_study_name(name) groups_arg = request.args.get("groups") group_ids = groups_arg.split(",") if groups_arg is not None else [] params = RequestParameters(user=Auth.get_current_user()) uuid = storage_service.create_study(name_sanitized, group_ids, params) content = "/studies/" + uuid code = HTTPStatus.CREATED.value return jsonify(content), code @bp.route("/studies/<string:uuid>/export", methods=["GET"]) @auth.protected() def export_study(uuid: str) -> Any: """ Export Study --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid stored in server schema: type: string - in: query name: no-output required: false example: false description: specify schema: type: boolean tags: - Manage Studies """ uuid_sanitized = sanitize_uuid(uuid) outputs: bool = ("no-output" not in request.args or request.args["no-output"] == "false") params = RequestParameters(user=Auth.get_current_user()) content = storage_service.export_study(uuid_sanitized, params, outputs) return send_file( content, mimetype="application/zip", as_attachment=True, attachment_filename=f"{uuid_sanitized}.zip", ) @bp.route("/studies/<string:uuid>", methods=["DELETE"]) @auth.protected() def delete_study(uuid: str) -> Any: """ Delete study --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string tags: - Manage Studies """ uuid_sanitized = sanitize_uuid(uuid) params = RequestParameters(user=Auth.get_current_user()) storage_service.delete_study(uuid_sanitized, params) content = "" code = HTTPStatus.NO_CONTENT.value return content, code @bp.route("/studies/<path:path>", methods=["POST"]) @auth.protected() def edit_study(path: str) -> Any: """ Update data --- responses: '200': description: Successful operation content: application/json: {} '404': description: File not found parameters: - in: path name: uuid required: true schema: type: string - in: path name: path schema: type: string required: true tags: - Manage Data inside Study """ new = json.loads(request.data) if not new: raise BadRequest("empty body not authorized") params = RequestParameters(user=Auth.get_current_user()) storage_service.edit_study(path, new, params) content = "" code = HTTPStatus.NO_CONTENT.value return content, code @bp.route( "/studies/<string:uuid>/output", methods=["POST"], ) @auth.protected() def import_output(uuid: str) -> Any: """ Import Output --- responses: '202': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string tags: - Manage Outputs """ uuid_sanitized = sanitize_uuid(uuid) if "output" not in request.files: content = "No data provided." code = HTTPStatus.BAD_REQUEST.value return content, code zip_binary = io.BytesIO(request.files["output"].read()) params = RequestParameters(user=Auth.get_current_user()) content = str( storage_service.import_output(uuid_sanitized, zip_binary, params)) code = HTTPStatus.ACCEPTED.value return jsonify(content), code @bp.route( "/studies/<string:uuid>/owner/<int:user_id>", methods=["PUT"], ) @auth.protected() def change_owner(uuid: str, user_id: int) -> Any: """ Change study owner --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string - in: path name: user_id required: true description: id of the new owner schema: type: number tags: - Manage Permissions """ uuid_sanitized = sanitize_uuid(uuid) params = RequestParameters(user=Auth.get_current_user()) storage_service.change_owner(uuid_sanitized, user_id, params) return "", HTTPStatus.OK @bp.route( "/studies/<string:uuid>/groups/<string:group_id>", methods=["PUT"], ) @auth.protected() def add_group(uuid: str, group_id: str) -> Any: """ Add a group association --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string - in: path name: group_id required: true description: id of the group to add schema: type: string tags: - Manage Permissions """ uuid_sanitized = sanitize_uuid(uuid) params = RequestParameters(user=Auth.get_current_user()) storage_service.add_group(uuid_sanitized, group_id, params) return "", HTTPStatus.OK @bp.route( "/studies/<string:uuid>/groups/<string:group_id>", methods=["DELETE"], ) @auth.protected() def remove_group(uuid: str, group_id: str) -> Any: """ Remove a group association --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string - in: path name: group_id required: true description: id of the group to remove schema: type: string tags: - Manage Permissions """ uuid_sanitized = sanitize_uuid(uuid) params = RequestParameters(user=Auth.get_current_user()) storage_service.remove_group(uuid_sanitized, group_id, params) return "", HTTPStatus.OK @bp.route( "/studies/<string:uuid>/public_mode/<string:mode>", methods=["PUT"], ) @auth.protected() def set_public_mode(uuid: str, mode: str) -> Any: """ Set study public mode --- responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request parameters: - in: path name: uuid required: true description: study uuid used by server schema: type: string - in: path name: mode required: true description: public mode schema: type: string enum: [NONE,READ,EXECUTE,EDIT,FULL] tags: - Manage Permissions """ uuid_sanitized = sanitize_uuid(uuid) params = RequestParameters(user=Auth.get_current_user()) public_mode = PublicMode(mode) storage_service.set_public_mode(uuid_sanitized, public_mode, params) return "", HTTPStatus.OK return bp
def create_utils_routes(storage_service: StorageService, config: Config) -> Blueprint: bp = Blueprint("create_utils", __name__) auth = Auth(config) @bp.route( "/file/<path:path>", methods=["GET"], ) @auth.protected() def get_file(path: str) -> Any: """ Get file --- responses: '200': content: application/octet-stream: {} description: Successful operation '404': description: File not found parameters: - in: path name: path required: true schema: type: string tags: - Manage Matrix """ try: params = RequestParameters(user=Auth.get_current_user()) return storage_service.get_matrix(path, params) except FileNotFoundError: return f"{path} not found", HTTPStatus.NOT_FOUND.value @bp.route( "/file/<path:path>", methods=["POST"], ) @auth.protected() def post_file(path: str) -> Any: """ Post file --- parameters: - in: path name: path required: true schema: type: string responses: '200': content: application/json: {} description: Successful operation '400': description: Invalid request tags: - Manage Matrix """ data = request.files["matrix"].read() params = RequestParameters(user=Auth.get_current_user()) storage_service.upload_matrix(path, data, params) output = b"" code = HTTPStatus.NO_CONTENT.value return output, code @bp.route("/health", methods=["GET"]) def health() -> Any: return jsonify({"status": "available"}), 200 @bp.route("/version", methods=["GET"]) def version() -> Any: """ Get application version --- responses: '200': content: application/json: schema: type: object properties: version: type: string description: Semantic version gitcommit: type: string description: Build version (git commit id) description: Successful operation tags: - Misc """ version_data = {"version": __version__} commit_id = get_commit_id(storage_service.study_service.path_resources) if commit_id is not None: version_data["gitcommit"] = commit_id return jsonify(version_data), HTTPStatus.OK.value @bp.after_request def after_request(response: Response) -> Response: header = response.headers header["Access-Control-Allow-Origin"] = "*" return response return bp