def wrapper(*args, **kwargs): try: user = None if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) elif include_gitlab_login and "X-Gitlab-Token" in request.headers: user = get_user_from_token(request.headers["X-Gitlab-Token"]) elif "access_token" in request.args: user = get_user_from_token(request.args.get("access_token")) if not user: return jsonify(message="User not signed in"), 401 except ValueError as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 403 return func(*args, **kwargs, user=user)
def gitlab_projects(): # noqa r"""Endpoint to retrieve GitLab projects. --- get: summary: Get user project from GitLab operationId: gitlab_projects description: >- Retrieve projects from GitLab. produces: - application/json responses: 200: description: >- This resource return all projects owned by the user on GitLab in JSON format. 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get("access_token")) secrets_store = REANAUserSecretsStore(str(user.id_)) gitlab_token = secrets_store.get_secret_value("gitlab_access_token") gitlab_user = secrets_store.get_secret_value("gitlab_user") gitlab_url = REANA_GITLAB_URL + "/api/v4/users/{0}/projects?access_token={1}" response = requests.get(gitlab_url.format(gitlab_user, gitlab_token)) projects = dict() if response.status_code == 200: for gitlab_project in response.json(): hook_id = _get_gitlab_hook_id(response, gitlab_project["id"], gitlab_token) projects[gitlab_project["id"]] = { "name": gitlab_project["name"], "path": gitlab_project["path_with_namespace"], "url": gitlab_project["web_url"], "hook_id": hook_id, } return jsonify(projects), 200 else: return ( jsonify({"message": "Project list could not be retrieved"}), response.status_code, ) except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def gitlab_webhook(): # noqa r"""Endpoint to setup a GitLab webhook. --- post: summary: Set a webhook on a user project from GitLab operationId: gitlab_webhook description: >- Setup a webhook for a GitLab project on GitLab. produces: - application/json responses: 201: description: >- The webhook was created. 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get('access_token')) secrets_store = REANAUserSecretsStore(str(user.id_)) gitlab_token = secrets_store.get_secret_value('gitlab_access_token') parameters = request.json gitlab_url = REANA_GITLAB_URL + "/api/v4/projects/" + \ "{0}/hooks?access_token={1}" webhook_payload = { "url": "https://{}/api/workflows".format(REANA_URL), "push_events": True, "push_events_branch_filter": "master", "merge_requests_events": True, "enable_ssl_verification": False, "token": user.access_token, } webhook = requests.post(gitlab_url.format( parameters['project_id'], gitlab_token), data=webhook_payload) return webhook.content, 201 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def get_me(): r"""Endpoint to get user information. --- get: summary: Gets information about authenticated user. description: >- This resource provides basic information about an authenticated user based on the session cookie presence. operationId: get_me produces: - application/json parameters: - name: access_token in: query description: API access_token of user. required: false type: string responses: 200: description: >- User information correspoding to the session cookie sent in the request. schema: type: object properties: email: type: string reana_token: type: string examples: application/json: { "email": "*****@*****.**", "reana_token": "Drmhze6EPcv0fN_81Bj-nA" } 401: description: >- Error message indicating that the uses is not authenticated. schema: type: object properties: error: type: string examples: application/json: { "error": "User not logged in" } 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Internal server error." } """ try: me = None if current_user.is_authenticated: me = _get_user_from_invenio_user(current_user.email) elif "access_token" in request.args: me = get_user_from_token(request.args.get('access_token')) if me: return (jsonify({ 'email': me.email, 'reana_token': me.access_token, 'full_name': me.full_name, 'username': me.username }), 200) else: return jsonify(message='User not logged in'), 401 except HTTPError as e: logging.error(traceback.format_exc()) return jsonify(e.response.json()), e.response.status_code except ValueError as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def add_secrets(): # noqa r"""Endpoint to create user secrets. --- post: summary: Add user secrets to REANA. description: >- This resource adds secrets for the authenticated user. operationId: add_secrets produces: - application/json parameters: - name: access_token in: query description: Secrets owner access token. required: false type: string - name: overwrite in: query description: Whether existing secret keys should be overwritten. required: false type: boolean - name: secrets in: body description: >- Optional. List of secrets to be added. required: true schema: type: object additionalProperties: type: object description: Secret definition. properties: name: type: string description: Secret name value: type: string description: Secret value type: type: string enum: - env - file description: >- How will be the secret assigned to the jobs, either exported as an environment variable or mounted as a file. responses: 201: description: >- Secrets successfully added. schema: type: object properties: message: type: string examples: application/json: { "message": "Secret(s) successfully added." } 403: description: >- Request failed. Token is not valid. examples: application/json: { "message": "Token is not valid" } 409: description: >- Request failed. Secrets could not be added due to a conflict. examples: application/json: { "message": "The submitted secrets api_key, password, username already exist." } 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Internal server error." } """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get('access_token')) secrets_store = REANAUserSecretsStore(str(user.id_)) overwrite = json.loads(request.args.get('overwrite')) secrets_store.add_secrets(request.json, overwrite=overwrite) return jsonify({"message": "Secret(s) successfully added."}), 201 except REANASecretAlreadyExists as e: return jsonify({"message": str(e)}), 409 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def delete_secrets(): # noqa r"""Endpoint to delete user secrets. --- delete: summary: Deletes the specified secret(s). description: >- This resource deletes the requested secrets. operationId: delete_secrets produces: - application/json parameters: - name: access_token in: query description: API key of the admin. required: false type: string - name: secrets in: body description: >- Optional. List of secrets to be deleted. required: true schema: type: array description: List of secret names to be deleted. items: type: string description: Secret name to be deleted. responses: 200: description: >- Secrets successfully deleted. schema: type: array description: List of secret names that have been deleted. items: type: string description: Name of the secret that have been deleted. examples: application/json: [ ".keytab", "username", ] 403: description: >- Request failed. Token is not valid. examples: application/json: { "message": "Token is not valid" } 404: description: >- Request failed. Secrets do not exist. schema: type: array description: List of secret names that could not be deleted. items: type: string description: Name of the secret which does not exist. examples: application/json: [ "certificate.pem", "PASSWORD", ] 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Internal server error." } """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get('access_token')) secrets_store = REANAUserSecretsStore(str(user.id_)) deleted_secrets_list = secrets_store.delete_secrets(request.json) return jsonify(deleted_secrets_list), 200 except REANASecretDoesNotExist as e: return jsonify(e.missing_secrets_list), 404 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def get_secrets(): # noqa r"""Endpoint to retrieve user secrets. --- get: summary: Get user secrets. Requires an user access token. description: >- Get user secrets. operationId: get_secrets produces: - application/json parameters: - name: access_token in: query description: Secrets owner access token. required: false type: string responses: 200: description: >- List of user secrets. schema: type: array items: properties: name: type: string description: Secret name type: type: string enum: - env - file description: >- How will be the secret assigned to the jobs, either exported as an environment variable or mounted as a file. examples: application/json: [ { "name": ".keytab", "value": "SGVsbG8gUkVBTkEh", }, { "name": "username", "value": "reanauser", }, ] 403: description: >- Request failed. Token is not valid. examples: application/json: { "message": "Token is not valid" } 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Error while querying." } """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get('access_token')) secrets_store = REANAUserSecretsStore(str(user.id_)) user_secrets = secrets_store.get_secrets() return jsonify(user_secrets), 200 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def gitlab_oauth(): # noqa r"""Endpoint to authorize REANA on GitLab. --- get: summary: Get access token from GitLab operationId: gitlab_oauth description: >- Authorize REANA on GitLab. produces: - application/json responses: 200: description: >- Ping succeeded. schema: type: object properties: message: type: string status: type: string examples: application/json: message: OK status: 200 201: description: >- Authorization succeeded. GitLab secret created. schema: type: object properties: message: type: string status: type: string examples: application/json: message: GitLab secret created status: 201 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get("access_token")) if "code" in request.args: # Verifies state parameter and obtain next url state_token = request.args.get("state") assert state_token # Checks authenticity and integrity of state and decodes the value. state = serializer.loads(state_token) # Verifies that state is for this session and that next parameter # has not been modified. assert state["sid"] == _create_identifier() # Stores next URL next_url = state["next"] gitlab_code = request.args.get("code") params = { "client_id": REANA_GITLAB_OAUTH_APP_ID, "client_secret": REANA_GITLAB_OAUTH_APP_SECRET, "redirect_uri": url_for(".gitlab_oauth", _external=True), "code": gitlab_code, "grant_type": "authorization_code", } gitlab_response = requests.post(REANA_GITLAB_URL + "/oauth/token", data=params).content secrets_store = REANAUserSecretsStore(str(user.id_)) secrets_store.add_secrets(_format_gitlab_secrets(gitlab_response), overwrite=True) return redirect(next_url), 201 else: return jsonify({"message": "OK"}), 200 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except (AssertionError, BadData): return jsonify({"message": "State param is invalid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def gitlab_webhook(): # noqa r"""Endpoint to setup a GitLab webhook. --- post: summary: Set a webhook on a user project from GitLab operationId: create_gitlab_webhook description: >- Setup a webhook for a GitLab project on GitLab. produces: - application/json parameters: - name: project_id in: path description: The GitLab project id. required: true type: integer responses: 201: description: >- The webhook was created. 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. delete: summary: Delete an existing webhook from GitLab operationId: delete_gitlab_webhook description: >- Remove an existing REANA webhook from a project on GitLab produces: - application/json parameters: - name: project_id in: path description: The GitLab project id. required: true type: integer - name: hook_id in: path description: The GitLab webhook id of the project. required: true type: integer responses: 204: description: >- The webhook was properly deleted. 404: description: >- No webhook found with provided id. 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get("access_token")) secrets_store = REANAUserSecretsStore(str(user.id_)) gitlab_token = secrets_store.get_secret_value("gitlab_access_token") parameters = request.json if request.method == "POST": gitlab_url = (REANA_GITLAB_URL + "/api/v4/projects/" + "{0}/hooks?access_token={1}") webhook_payload = { "url": "https://{}/api/workflows".format(REANA_URL), "push_events": True, "push_events_branch_filter": "master", "merge_requests_events": True, "enable_ssl_verification": False, "token": user.access_token, } webhook = requests.post( gitlab_url.format(parameters["project_id"], gitlab_token), data=webhook_payload, ) return jsonify({"id": webhook.json()["id"]}), 201 elif request.method == "DELETE": gitlab_url = (REANA_GITLAB_URL + "/api/v4/projects/" + "{0}/hooks/{1}?access_token={2}") resp = requests.delete( gitlab_url.format(parameters["project_id"], parameters["hook_id"], gitlab_token)) return resp.content, resp.status_code except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def request_token(): r"""Endpoint to request user access token. --- put: summary: Requests a new access token for the authenticated user. description: >- This resource allows the user to create an empty REANA access token and mark it as requested. operationId: request_token produces: - application/json parameters: - name: access_token in: query description: API access_token of user. required: false type: string responses: 200: description: >- User information correspoding to the session cookie sent in the request. schema: type: object properties: reana_token: type: object properties: status: type: string requested_at: type: string examples: application/json: { "reana_token": { "status": "requested", "requested_at": "Mon, 25 May 2020 10:45:15 GMT" } } 401: description: >- Error message indicating that the uses is not authenticated. schema: type: object properties: error: type: string examples: application/json: { "error": "User not logged in" } 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Internal server error." } """ try: user = None if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) elif "access_token" in request.args: user = get_user_from_token(request.args.get("access_token")) user.request_access_token() user.log_action(AuditLogAction.request_token) email_subject = f"[{REANA_URL}] Token request ({user.email})" fields = [ "id_", "email", "full_name", "username", "access_token", "access_token_status", ] email_body = "New user access token request:\n\n" + "\n".join( [f"{f}: {getattr(user, f, None)}" for f in fields] ) send_email(ADMIN_EMAIL, email_subject, email_body) return ( jsonify( { "reana_token": { "status": user.access_token_status, "requested_at": user.latest_access_token.created, } } ), 200, ) except HTTPError as e: logging.error(traceback.format_exc()) return jsonify(e.response.json()), e.response.status_code except ValueError as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def get_me(): r"""Endpoint to get user information. --- get: summary: Gets information about authenticated user. description: >- This resource provides basic information about an authenticated user based on the session cookie presence. operationId: get_me produces: - application/json parameters: - name: access_token in: query description: API access_token of user. required: false type: string responses: 200: description: >- User information correspoding to the session cookie sent in the request. schema: type: object properties: email: type: string reana_token: type: object properties: value: type: string status: type: string requested_at: type: string examples: application/json: { "email": "*****@*****.**", "reana_token": { "value": "Drmhze6EPcv0fN_81Bj-nA", "status": "active", "requested_at": "Mon, 25 May 2020 10:39:57 GMT", }, "full_name": "John Doe", "username": "******" } 401: description: >- Error message indicating that the uses is not authenticated. schema: type: object properties: error: type: string examples: application/json: { "error": "User not logged in" } 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal server error. examples: application/json: { "message": "Internal server error." } """ try: me = None if current_user.is_authenticated: me = _get_user_from_invenio_user(current_user.email) elif "access_token" in request.args: me = get_user_from_token(request.args.get("access_token")) if me: return ( jsonify( { "email": me.email, "reana_token": { "value": me.access_token, "status": me.access_token_status, "requested_at": me.latest_access_token.created if me.latest_access_token else None, }, "full_name": me.full_name, "username": me.username, "announcement": REANA_UI_ANNOUNCEMENT, } ), 200, ) return jsonify(message="User not logged in"), 401 except HTTPError as e: logging.error(traceback.format_exc()) return jsonify(e.response.json()), e.response.status_code except ValueError as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500
def gitlab_oauth(): # noqa r"""Endpoint to authorize REANA on GitLab. --- get: summary: Get access token from GitLab operationId: gitlab_oauth description: >- Authorize REANA on GitLab. produces: - application/json responses: 200: description: >- Ping succeeded. schema: type: object properties: message: type: string status: type: string examples: application/json: message: OK status: 200 201: description: >- Authorization succeeded. GitLab secret created. schema: type: object properties: message: type: string status: type: string examples: application/json: message: GitLab secret created status: 201 403: description: >- Request failed. User token not valid. examples: application/json: { "message": "Token is not valid." } 500: description: >- Request failed. Internal controller error. """ try: if current_user.is_authenticated: user = _get_user_from_invenio_user(current_user.email) else: user = get_user_from_token(request.args.get('access_token')) if 'code' in request.args: gitlab_code = request.args.get('code') parameters = "client_id={0}&" + \ "client_secret={1}&code={2}&" + \ "grant_type=authorization_code&redirect_uri={3}" parameters = parameters.format(REANA_GITLAB_OAUTH_APP_ID, REANA_GITLAB_OAUTH_APP_SECRET, gitlab_code, REANA_GITLAB_OAUTH_REDIRECT_URL) gitlab_response = requests.post(REANA_GITLAB_URL + '/oauth/token', data=parameters).content secrets_store = REANAUserSecretsStore(str(user.id_)) secrets_store.add_secrets(_format_gitlab_secrets(gitlab_response), overwrite=True) return jsonify({"message": "GitLab secret created"}), 201 else: return jsonify({"message": "OK"}), 200 except ValueError: return jsonify({"message": "Token is not valid."}), 403 except Exception as e: logging.error(traceback.format_exc()) return jsonify({"message": str(e)}), 500