diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf3b457..c459c36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.13.0] - 2022-04-04 +### Fixed +- UI changes to 'Source Repo URL' field in catalog. [#814](https://github.com/rokwire/rokwire-building-blocks-api/issues/814) +- Talent form bug fixed in Contribution Catalog. [#857](https://github.com/rokwire/rokwire-building-blocks-api/issues/857) +- `imageURL` field name in Events BB API Doc. [#846](https://github.com/rokwire/rokwire-building-blocks-api/issues/846) +- Build error in Events BB. [#884](https://github.com/rokwire/rokwire-building-blocks-api/issues/884) + +### Added +- Add a Review section to Contribution. [#756](https://github.com/rokwire/rokwire-building-blocks-api/issues/756) +- Error message text to event building block when it is internal sever error. [#840](https://github.com/rokwire/rokwire-building-blocks-api/issues/840) +- Added email field for reviewers. [#839](https://github.com/rokwire/rokwire-building-blocks-api/issues/839) +- Add query filter to get events created by a user. [#873](https://github.com/rokwire/rokwire-building-blocks-api/issues/873) +- Add Aquatics and Intramural tags. [#872](https://github.com/rokwire/rokwire-building-blocks-api/issues/872) + +### Changed +- All group event user can GET and DELETE group event. [#847](https://github.com/rokwire/rokwire-building-blocks-api/issues/847) +- CODEOWNERS file. [#826](https://github.com/rokwire/rokwire-building-blocks-api/issues/826) + +### Security +- Change string comparisons to constant time comparisons in Profile BB. [#850](https://github.com/rokwire/rokwire-building-blocks-api/issues/850) + ## [1.12.1] - 2021-12-08 ### Changed - Contributions Catalog login callback endpoint to /catalog/auth/callback. [#803](https://github.com/rokwire/rokwire-building-blocks-api/issues/803) @@ -17,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - String comparisons to constant time comparisons in auth middleware library. [#825](https://github.com/rokwire/rokwire-building-blocks-api/issues/825) +- Removed unused method in token utility. [#837](https://github.com/rokwire/rokwire-building-blocks-api/issues/837) ## [1.12.0] - 2021-11-19 ### Added @@ -425,7 +447,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - References to AWS keys and variables in the Events Building Block. -[Unreleased]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.12.1...HEAD +[Unreleased]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.13.0...HEAD +[1.13.0]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.12.1...1.13.0 [1.12.1]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.12.0...1.12.1 [1.12.0]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.11.3...1.12.0 [1.11.3]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.11.2...1.11.3 @@ -451,4 +474,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [1.0.3]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.0.2...1.0.3 [1.0.2]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.0.0...1.0.1 -[1.0.0]: https://github.com/rokwire/rokwire-building-blocks-api/releases/tag/1.0.0 +[1.0.0]: https://github.com/rokwire/rokwire-building-blocks-api/releases/tag/1.0.0 \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 1f3c63f2..e65141f5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,31 +1,28 @@ # Default assignment -* @sandeep-ps +* @sandeep-ps # App Config Building Block -/appconfigservice/ @wenjzhu - -# Authentication Building Block -/authservice/ @wenjzhu +/appconfigservice/ @minump # Authentication Middleware Library -/lib/auth-middleware/ @ywkim312 +/lib/auth-middleware/ @ywkim312 # API Doc /Dockerfile @sandeep-ps /README @sandeep-ps # Events Building Block -/eventservice/ @bingzhang +/eventservice/ @bingzhang # Logging Building Block -/loggingservice/ @ywkim312 +/loggingservice/ @ywkim312 # Profile Building Block -/profileservice/ @ywkim312 +/profileservice/ @ywkim312 # Contributions Building Block -/contributions/* @ywkim312 -/contributions/api/ @ywkim312 +/contributions/* @ywkim312 +/contributions/api/ @ywkim312 # Contributions Catalog -/contributions/catalog/ @wenjzhu +/contributions/catalog/ @minump diff --git a/SECURITY.md b/SECURITY.md index d87a62f5..2ea78eda 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,6 +6,7 @@ Patches for **Rokwire Building Blocks** in this repository will only be applied | Version | Supported | | ------- | ------------------ | +| 1.13.0 | :white_check_mark: | | 1.12.1 | :white_check_mark: | | 1.12.0 | :white_check_mark: | | 1.11.3 | :white_check_mark: | diff --git a/contributions/api/controllers/contributions.py b/contributions/api/controllers/contributions.py index 46d94303..641435c8 100644 --- a/contributions/api/controllers/contributions.py +++ b/contributions/api/controllers/contributions.py @@ -173,15 +173,16 @@ def post(token_info): required_cap_list = talent.requiredCapabilities for capability_json in required_cap_list: capability, rest_capability_json, msg = modelutils.construct_required_capability(capability_json) - is_uuid = otherutils.check_if_uuid(capability.id) - if not is_uuid: - msg = { - "reason": "Capability id in requiredCapabilities is not in uuid format", - "error": "Bad Request: " + request.url, - } - msg_json = jsonutils.create_log_json("Contribution", "POST", msg) - logging.error("Contribution POST " + json.dumps(msg_json)) - return rs_handlers.bad_request(msg) + if capability.id is not None: + is_uuid = otherutils.check_if_uuid(capability.id) + if not is_uuid: + msg = { + "reason": "Capability id in requiredCapabilities is not in uuid format", + "error": "Bad Request: " + request.url, + } + msg_json = jsonutils.create_log_json("Contribution", "POST", msg) + logging.error("Contribution POST " + json.dumps(msg_json)) + return rs_handlers.bad_request(msg) talent_list.append(talent) contribution_dataset.set_talents(talent_list) except: @@ -262,7 +263,10 @@ def get(token_info=None, id=None): if is_error: return resp jsonutils.convert_obejctid_from_dataset_json(data_list[0]) - out_json = mongoutils.construct_json_from_query_list(data_list[0]) + + out_json = mongoutils.construct_json_from_query_list(data_list[0], login_id=login_id) + + msg_json = jsonutils.create_log_json("Contribution", "GET", {"id": str(id)}) logging.info("Contribution GET " + json.dumps(jsonutils.remove_objectid_from_dataset(msg_json))) @@ -359,15 +363,16 @@ def put(token_info, id): required_cap_list = talent.requiredCapabilities for capability_json in required_cap_list: capability, rest_capability_json, msg = modelutils.construct_capability(capability_json) - is_uuid = otherutils.check_if_uuid(capability.id) - if not is_uuid: - msg = { - "reason": "Capability id in requiredCapabilities is not in uuid format", - "error": "Bad Request: " + request.url, - } - msg_json = jsonutils.create_log_json("Contribution", "POST", msg) - logging.error("Contribution POST " + json.dumps(msg_json)) - return rs_handlers.bad_request(msg) + if capability.id is not None: + is_uuid = otherutils.check_if_uuid(capability.id) + if not is_uuid: + msg = { + "reason": "Capability id in requiredCapabilities is not in uuid format", + "error": "Bad Request: " + request.url, + } + msg_json = jsonutils.create_log_json("Contribution", "POST", msg) + logging.error("Contribution POST " + json.dumps(msg_json)) + return rs_handlers.bad_request(msg) talent_list.append(talent) contribution_dataset.set_talents(talent_list) @@ -388,7 +393,7 @@ def put(token_info, id): out_json = contribution_dataset msg_json = jsonutils.create_log_json("Contribution", "PUT", {"id": str(id)}) logging.info("Contribution PUT " + json.dumps(msg_json)) - out_json = mongoutils.construct_json_from_query_list(out_json) + out_json = mongoutils.construct_json_from_query_list(out_json, login_id=token_info["login"]) return out_json @@ -853,6 +858,7 @@ def admin_reviewers_post(token_info): in_json = request.get_json() name = in_json["name"] username = in_json["githubUsername"] + email = in_json["email"] # check if the dataset is existing with given github username dataset = mongoutils.get_dataset_from_field(coll_reviewer, "githubUsername", username) diff --git a/contributions/api/models/capabilities/capability.py b/contributions/api/models/capabilities/capability.py index 24a89bb9..330fa661 100644 --- a/contributions/api/models/capabilities/capability.py +++ b/contributions/api/models/capabilities/capability.py @@ -20,6 +20,7 @@ def __init__(self, injson): self.name = None self.description = None self.isOpenSource = None + self.sourceRepoUrl = None self.apiDocUrl = None self.deploymentDetails = None self.apiBaseUrl = None @@ -56,6 +57,12 @@ def set_is_open_source(self, isOpenSource): def get_is_open_source(self): return self.isOpenSource + def set_source_repo_url(self, sourceRepoUrl): + self.sourceRepoUrl = sourceRepoUrl + + def get_source_repo_url(self): + return self.sourceRepoUrl + def set_api_doc_url(self, apiDocUrl): self.apiDocUrl = apiDocUrl diff --git a/contributions/api/models/contribution.py b/contributions/api/models/contribution.py index 7d4750cf..4c4f2b97 100644 --- a/contributions/api/models/contribution.py +++ b/contributions/api/models/contribution.py @@ -25,6 +25,7 @@ def __init__(self, injson): self.capabilities = None self.talents = None self.status = None + self.review = None self.dateCreated = None self.dateModified = None @@ -72,6 +73,12 @@ def set_status(self, status): def get_status(self): return self.status + def set_review(self, review): + self.review = review + + def get_review(self): + return self.review + def set_contributors(self, contributors): self.contributors = contributors diff --git a/contributions/api/models/review.py b/contributions/api/models/review.py new file mode 100644 index 00000000..be94d0b5 --- /dev/null +++ b/contributions/api/models/review.py @@ -0,0 +1,41 @@ +# Copyright 2020 Board of Trustees of the University of Illinois. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import utils.datasetutils as datasetutils + +class Review: + def __init__(self, injson): + self.reviewerId = None + self.reviewComment = None + self.reviewLastUpdated = None + + self, restjson = datasetutils.update_review_dataset_from_json(self, injson) + + def set_reviewer_id(self, reviewerId): + self.reviewerId = reviewerId + + def get_reviewer_id(self): + return self.reviewerId + + def set_review_comment(self, reviewComment): + self.reviewComment = reviewComment + + def get_review_comment(self): + return self.reviewComment + + def set_review_last_updated(self, reviewLastUpdated): + self.reviewLastUpdated = reviewLastUpdated + + def get_review_last_updated(self): + return self.reviewLastUpdated diff --git a/contributions/api/models/reviewer.py b/contributions/api/models/reviewer.py index ee2eb2c2..64d00f87 100644 --- a/contributions/api/models/reviewer.py +++ b/contributions/api/models/reviewer.py @@ -18,6 +18,7 @@ class Reviewer: def __init__(self, injson): self.name = None self.githubUsername = None + self.email = None self.dateCreated = None self, restjson = datasetutils.update_reviwer_dataset_from_json(self, injson) @@ -34,6 +35,12 @@ def set_github_username(self, githubUsername): def get_github_username(self): return self.githubUsername + def set_email(self, email): + self.email = email + + def get_email(self): + return self.email + def set_date_created(self, dateCreated): self.dateCreated = dateCreated diff --git a/contributions/api/utils/datasetutils.py b/contributions/api/utils/datasetutils.py index 33f5bfa6..60c82e6a 100644 --- a/contributions/api/utils/datasetutils.py +++ b/contributions/api/utils/datasetutils.py @@ -98,6 +98,11 @@ def update_contribution_dataset_from_json(dataset, injson): del outjson['talents'] except: pass + try: + dataset.set_review(injson['review']) + del outjson['review'] + except: + pass try: dataset.set_status(injson["status"]) del outjson["status"] @@ -141,6 +146,11 @@ def update_capability_dataset_from_json(dataset, injson): del outjson['isOpenSource'] except: pass + try: + dataset.set_source_repo_url(injson['sourceRepoUrl']) + del outjson['sourceRepoUrl'] + except: + pass try: dataset.set_api_doc_url(injson['apiDocUrl']) del outjson['apiDocUrl'] @@ -428,6 +438,29 @@ def update_talent_dataset_from_json(dataset, injson): return dataset, outjson +""" +update review dataset +""" +def update_review_dataset_from_json(dataset, injson): + outjson = copy.copy(injson) + try: + dataset.set_reviewer_id(injson['reviewerId']) + del outjson['reviewerId'] + except: + pass + try: + dataset.set_review_comment(injson['reviewComment']) + del outjson['reviewComment'] + except: + pass + try: + dataset.set_review_comment(injson['reviewLastUpdate']) + del outjson['reviewLastUpdated'] + except: + pass + + return dataset, outjson + """ update required capability dataset """ @@ -466,5 +499,10 @@ def update_reviwer_dataset_from_json(dataset, injson): del outjson['githubUsername'] except: pass + try: + dataset.set_email(injson['email']) + del outjson['email'] + except: + pass return dataset, outjson diff --git a/contributions/api/utils/mongoutils.py b/contributions/api/utils/mongoutils.py index 6d162c9c..17d215f8 100644 --- a/contributions/api/utils/mongoutils.py +++ b/contributions/api/utils/mongoutils.py @@ -23,6 +23,7 @@ from models.contribution import Contribution from utils import query_params from utils import jsonutils +from utils import adminutils client_contribution = MongoClient(cfg.MONGO_CONTRIBUTION_URL, connect=False) db_contribution = client_contribution[cfg.CONTRIBUTION_DB_NAME] @@ -304,8 +305,15 @@ def query_dataset_no_status(db_collection, fld, query_str): """ construct json from mongo query """ -def construct_json_from_query_list(data_list): - data_dump = dumps(data_list) +def construct_json_from_query_list(in_json, login_id=None): + # check if the user is a reviewer or admin, otherwise, remove review from the output + if login_id is not None: + is_reviewer = adminutils.check_if_reviewer(login_id) + # remove review if the requested user is not reviewer + if not is_reviewer: + if "review" in in_json: + del in_json["review"] + data_dump = dumps(in_json) out_json = make_response(data_dump) out_json.mimetype = 'application/json' diff --git a/contributions/api/utils/tokenutils.py b/contributions/api/utils/tokenutils.py index 202f7fb4..bf99863f 100644 --- a/contributions/api/utils/tokenutils.py +++ b/contributions/api/utils/tokenutils.py @@ -17,20 +17,12 @@ def get_id_info_from_token(in_token): id_type = 0 # 0 for no pii, 1 for uin id, 2 phone id id_string = "" - # check if the pii token is from campus or from outside the campus # if there is uin, it is from campus if 'uiucedu_uin' in in_token: id_string = in_token['uiucedu_uin'] id_type = 1 - # TODO following lines are modified to use email instead of uin in id checking - # for GET, POST, and DELETE - # # if there is email, it is from campus - # if 'email' in in_token: - # id_string = in_token['email'] - # id_type = 1 - # if there is phone number, it is from outside the campus if 'phoneNumber' in in_token: id_string = in_token['phoneNumber'] @@ -39,16 +31,6 @@ def get_id_info_from_token(in_token): return id_type, id_string -def convert_str_to_dict(in_token): - while True: - try: - converted_dict = eval(in_token) - except NameError as e: - key = e.message.split("'")[1] - in_token = in_token.replace(key, "'{}'".format(key)) - else: - return converted_dict - def get_data_from_token(in_token): tk_uin = in_token.get('uiucedu_uin') tk_is_uin = tk_uin is not None diff --git a/contributions/catalog/controllers/contribute.py b/contributions/catalog/controllers/contribute.py index 36e30112..338895bf 100644 --- a/contributions/catalog/controllers/contribute.py +++ b/contributions/catalog/controllers/contribute.py @@ -16,6 +16,7 @@ import logging import traceback import requests +import datetime from flask import ( Blueprint, render_template, request, session, redirect, url_for @@ -24,10 +25,10 @@ from formencode import variabledecode from .auth import login_required from controllers.config import Config as cfg -from models.contribution_utilities import to_contribution from utils import jsonutil from utils import adminutil from utils import requestutil +import models.contribution_utilities as cont_util import os @@ -151,26 +152,32 @@ def contribution_details(contribution_id): except: is_logged_in = False + is_editor = False is_reviewer = False name = "" if (is_logged_in): # check to see if the logged in user is an editable user for creating edit button - is_editable = False username = session["username"] + name = session["name"] headers = requestutil.get_header_using_session(session) - is_editable = adminutil.check_if_reviewer(username, headers) the_json_res = get_contribution(contribution_id) + + # check if the logged in user is the editor + is_superuser = adminutil.check_if_superuser(username) + if is_superuser: + is_editor = True + elif username in the_json_res["contributionAdmins"]: + is_editor = True + # check if the user is reviewer by requesting to endpoint - username = session["username"] - name = session["name"] - headers = requestutil.get_header_using_session(session) is_reviewer = adminutil.check_if_reviewer(username, headers) else: the_json_res = get_contribution_with_api_key(contribution_id) - return render_template("contribute/contribution_details.html", reviewer=is_reviewer, post=the_json_res, user=name) + return render_template("contribute/contribution_details.html", is_reviewer=is_reviewer, is_editable=is_editor, + post=the_json_res, user=name) @bp.route('/contributions//edit', methods=['GET', 'POST']) @login_required @@ -189,7 +196,7 @@ def contribution_edit(contribution_id): return render_template('contribute/error.html', error_msg=s) if is_put: - contribution = to_contribution(result) + contribution = cont_util.to_contribution(result) contribution = jsonutil.add_contribution_admins(contribution, is_edit=True) # remove id from json_data del contribution["id"] @@ -212,21 +219,134 @@ def contribution_edit(contribution_id): the_json_res = get_contribution(contribution_id) # check if the user is editable then set the is_editable is_editable = False + is_reviewer = False username = session["username"] headers = requestutil.get_header_using_session(session) - is_editable = adminutil.check_if_reviewer(username, headers) + + # check if the user is in contribution's admins + if username in the_json_res["contributionAdmins"]: + is_editable = True + + # check if the user is a reviewer + is_reviewer = adminutil.check_if_reviewer(username, headers) + + if is_reviewer: + is_editable = True # get capability list to create required capability list required_capability_list = requestutil.request_required_capability_list(headers) if is_editable: - return render_template('contribute/contribute.html', required_capabilities=required_capability_list, + return render_template('contribute/contribute.html', is_reviewer=is_reviewer, required_capabilities=required_capability_list, is_editable=is_editable, user=session["name"], token=session['oauth_token']['access_token'], post=the_json_res) else: s = "You don't have a permission to edit the contribution." return render_template('contribute/error.html', error_msg=s) +@bp.route('/contributions//review', methods=['GET', 'POST']) +@login_required +def contribution_review(contribution_id): + username = session["username"] + headers = requestutil.get_header_using_session(session) + + # check if the user is a reviewer + is_reviewer = adminutil.check_if_reviewer(username, headers) + + if request.method == 'POST': + is_put = False + contribution_id = None + result = request.form.to_dict(flat=False) + + # check if it is PUT + try: + contribution_status = result["contribution_status"][0] + contribution_comment = result["contribution_reviewer_comment"][0] + contribution_id = result["contribution_id"][0] + is_put = True + except: + s = "There is a error in edit. The method is not an edit." + return render_template('contribute/error.html', error_msg=s) + + if is_put: + # check reviewer id from token + reviewer_id = adminutil.get_reviewer_id(headers, username) + if reviewer_id.lower == "none": + s = "You don't have a permission to review the contribution." + return render_template('contribute/error.html', error_msg=s) + contribution_result = requestutil.request_single_contribution(headers, contribution_id) + contribution = json.loads(contribution_result.text) + + # remove id from json_data + del contribution["id"] + + currenttime = datetime.datetime.now() + currenttime = currenttime.strftime("%Y/%m/%dT%H:%M:%S") + + # iterate reviews to find out the correct review location + review_loc = 0 + if "review" in contribution.keys() and contribution["review"] is not None: + review_list = contribution["review"] + for idx, review_entry in enumerate(review_list): + if review_entry["reviewerId"] == reviewer_id: + review_loc = idx + # add new elements(review_json) + currenttime = datetime.datetime.now() + currenttime = currenttime.strftime("%Y/%m/%dT%H:%M:%S") + contribution["review"][review_loc]["lastUpdated"] = currenttime + contribution["review"][review_loc]["reviewerComment"] = contribution_comment + contribution["review"][review_loc]["reviewerId"] = reviewer_id + else: + # add new elements + review_json = {'review':[{'lastUpdated': currenttime, 'reviewerComment': contribution_comment, + 'reviewerId': reviewer_id}]} + contribution.update(review_json) + contribution["status"] = contribution_status + json_contribution = json.dumps(contribution, indent=4) + response, s = put_contribution(json_contribution, contribution_id) + + if response: + if "name" in session: + return redirect(url_for('contribute.contribution_details', contribution_id=contribution_id)) + else: + return render_template('contribute/submitted.html') + elif not response: + if "name" in session: + return render_template('contribute/error.html', user=session["name"], + token=session['oauth_token']['access_token'], error_msg=s) + else: + return render_template('contribute/error.html', error_msg=s) + else: + the_json_res = get_contribution(contribution_id) + + # check if the user is in contribution's admins + if username in the_json_res["contributionAdmins"]: + is_editable = True + + # get capability list to create required capability list + required_capability_list = requestutil.request_required_capability_list(headers) + + # if is_reviewer and is_review: + if is_reviewer: + # need to add review information to html so it can pre render if needed + reviewer_id = adminutil.get_reviewer_id(headers, username) + if "review" in the_json_res.keys(): + review_exist = False + review_list = the_json_res["review"] + if review_list is not None: + for idx, review_entry in enumerate(review_list): + if review_entry["reviewerId"] == reviewer_id: + review_loc = idx + review_json = {'review': review_list[review_loc]} + the_json_res.update(review_json) + break + + return render_template('contribute/contribution_details.html', is_review=True, required_capabilities=required_capability_list, + user=session["name"], token=session['oauth_token']['access_token'], post=the_json_res) + else: + s = "You don't have a permission to review the contribution." + return render_template('contribute/error.html', error_msg=s) + @bp.route('/contributions//capabilities/', methods=['GET']) def capability_details(contribution_id, id): # check if the user is logged in @@ -320,7 +440,7 @@ def create(): result = request.form.to_dict(flat=False) # result = dict((key, request.form.getlist(key) if len(request.form.getlist(key)) > 1 else request.form.getlist(key)[0]) for key in request.form.keys()) - contribution = to_contribution(result) + contribution = cont_util.to_contribution(result) # add contributionAdmins to the json_contribution contribution = jsonutil.add_contribution_admins(contribution) contribution["status"] = "Submitted" @@ -344,6 +464,48 @@ def create(): return render_template('contribute/contribute.html', required_capabilities=required_capability_list, post=json_contribute, user=session["name"], token=session['oauth_token']['access_token']) +# reviewers page +@bp.route('/contributions/reviews', methods=['GET']) +@login_required +def reviews_main(): + show_sel = request.args.get('show') + user = None + token = None + is_logged_in = False + is_editable = False + the_res_json = None + + try: + # create error to see if the user is logged in or now + # TODO this should be changed to better way + if (session["name"] == ""): + is_logged_in = True + else: + is_logged_in = True + user = session["name"] + token = session['oauth_token']['access_token'] + except: + is_logged_in = False + + if is_logged_in: + # check if the user is editable then set the is_editable + username = session["username"] + headers = requestutil.get_header_using_session(session) + is_editable = adminutil.check_if_reviewer(username, headers) + + if is_editable: + result = requestutil.request_contributions(headers) + if show_sel == None or show_sel == "all": + # create the json for only submitted + the_json_res = result.json() + else: + # create the json for keyword related + the_json_res = jsonutil.create_status_json_from_contribution_json(result.json(), show_sel) + return render_template('contribute/reviews.html', is_editable=is_editable, user=session["name"], token=session['oauth_token']['access_token'], post=the_json_res) + else: + s = "You don't have a permission to edit the contribution." + return render_template('contribute/error.html', error_msg=s) + @bp.errorhandler(404) def page_not_found(e): # note that we set the 404 status explicitly @@ -361,6 +523,22 @@ def submitted(): def search_results(search): return render_template('results.html', results=results, user=session["name"], token=session['oauth_token']['access_token']) +# get all contributions +def get_contributions(): + headers = requestutil.get_header_using_session(session) + + try: + result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL, headers=headers) + + if result.status_code != 200: + err_json = parse_response_error(result) + logging.error("Contribution GET " + json.dumps(err_json)) + return {} + + except Exception: + # traceback.print_exc() + return False + return result.json() # post a json_data in a http request def post_contribution(json_data): diff --git a/contributions/catalog/models/capability_utilities.py b/contributions/catalog/models/capability_utilities.py index 18f44bfc..3c967b8b 100644 --- a/contributions/catalog/models/capability_utilities.py +++ b/contributions/catalog/models/capability_utilities.py @@ -20,8 +20,8 @@ def init_capability(): 'description': '', 'icon': None, 'apiDocUrl': None, - 'isOpenSource': False, - 'sourceUrl': '', + 'isOpenSource': None, + 'sourceRepoUrl': '', 'apiBaseUrl': None, 'version': '', 'healthCheckUrl': None, @@ -61,11 +61,14 @@ def to_capability(d): for k, v in d.items(): if "isOpenSource" in k: - if v[i] == 'on': + if v[i] == 'y': capability_list[i]["isOpenSource"] = True else: capability_list[i]["isOpenSource"] = False d[k][i] = capability_list[i]["isOpenSource"] + elif "sourceRepoUrl" in k: + if capability_list[i]["isOpenSource"]: + capability_list[i]["sourceRepoUrl"] = v[i] elif "deploymentDetails_" in k: name = k.split("deploymentDetails_")[-1] capability_list[i]["deploymentDetails"][name] = v[i] diff --git a/contributions/catalog/models/talent_utilities.py b/contributions/catalog/models/talent_utilities.py index 334462a6..528cf9d1 100644 --- a/contributions/catalog/models/talent_utilities.py +++ b/contributions/catalog/models/talent_utilities.py @@ -36,42 +36,42 @@ def init_talent(): def to_talent(d): if not d: return {} talent_list = [] - - if isinstance(d['talent_name'], str): - talent_list.append(init_talent()) - else: - for _ in range(len(d['talent_name'])): + if 'talent_name' in d.keys(): + if isinstance(d['talent_name'], str): talent_list.append(init_talent()) + else: + for _ in range(len(d['talent_name'])): + talent_list.append(init_talent()) - for i, talent in enumerate(talent_list): - tal_id = str(uuid.uuid4()) - talent['id'] = tal_id + for i, talent in enumerate(talent_list): + tal_id = str(uuid.uuid4()) + talent['id'] = tal_id - for k, v in d.items(): - # print(k, v) - if "minUserPrivacyLevel" in k: - if len(v[i]) > 0: - talent_list[i]["minUserPrivacyLevel"] = int(v[i]) - d[k][i] = talent_list[i]["minUserPrivacyLevel"] - if "talent_" in k: - name = k.split("talent_")[-1] - # TODO this is not a very good exercise since everything is list, - # so the code only checks the very first items in the list assuming that - # the items are only single item entry. - # However, required capabilities should be a list so it should be handled differently, - # and if there is any item that is a list, that should be handled separately. - if name in talent_list[i] and isinstance(talent_list[i][name], list) and len(v[i]) > 0: - if name == "requiredCapabilities": - for j in range(len(v)): - v[j] = reconstruct_required_capabilities(v[j]) - talent_list[i][name].append(v[j]) + for k, v in d.items(): + # print(k, v) + if "minUserPrivacyLevel" in k: + if len(v[i]) > 0: + talent_list[i]["minUserPrivacyLevel"] = int(v[i]) + d[k][i] = talent_list[i]["minUserPrivacyLevel"] + if "talent_" in k: + name = k.split("talent_")[-1] + # TODO this is not a very good exercise since everything is list, + # so the code only checks the very first items in the list assuming that + # the items are only single item entry. + # However, required capabilities should be a list so it should be handled differently, + # and if there is any item that is a list, that should be handled separately. + if name in talent_list[i] and isinstance(talent_list[i][name], list) and len(v[i]) > 0: + if name == "requiredCapabilities": + for j in range(len(v)): + v[j] = reconstruct_required_capabilities(v[j]) + talent_list[i][name].append(v[j]) + else: + talent_list[i][name].append(v[i]) + elif name in talent_list[i] and isinstance(talent_list[i][name], list) and len(v[i]) == 0: + talent_list[i][name] = [] else: - talent_list[i][name].append(v[i]) - elif name in talent_list[i] and isinstance(talent_list[i][name], list) and len(v[i]) == 0: - talent_list[i][name] = [] - else: - talent_list[i][name] = v[i] - talent_list[i]["selfCertification"] = to_self_certification(d) + talent_list[i][name] = v[i] + talent_list[i]["selfCertification"] = to_self_certification(d) return talent_list def init_self_certification(): diff --git a/contributions/catalog/utils/adminutil.py b/contributions/catalog/utils/adminutil.py index 1e9fae13..c8a0416b 100644 --- a/contributions/catalog/utils/adminutil.py +++ b/contributions/catalog/utils/adminutil.py @@ -29,15 +29,31 @@ def check_if_superuser(login_id): return False """ -check if the logged in user is a reviewer +get reviewer id from given username """ -def check_if_reviewer(login_id, headers): - # check if the logged in id is admin user - is_superuser = check_if_superuser(login_id) +def get_reviewer_id(headers, username): + # check if the logged in id is in reviewers database + # otherwise it will give 401 + reviewer_id = "None" + result = requestutil.request_reviewers(headers) - if is_superuser: - return True + # if the result is not 201, it is not reviewer + if result.status_code == 200: + # check if the login id is in reviewer list + result_str = result.content.decode('utf-8').replace('\n', '') + reviewer_list = json.loads(result_str) + + # check if the user name and id + for reviewer in reviewer_list: + if reviewer["githubUsername"] == username: + reviewer_id = reviewer["id"] + return reviewer_id + +""" +check if the logged in user is a reviewer +""" +def check_if_reviewer(login_id, headers): # check if the logged in id is in reviewers database # otherwise it will give 401 result = requestutil.request_reviewers(headers) @@ -57,5 +73,3 @@ def check_if_reviewer(login_id, headers): return True return False - - diff --git a/contributions/catalog/utils/jsonutil.py b/contributions/catalog/utils/jsonutil.py index a37dbaaf..703686aa 100644 --- a/contributions/catalog/utils/jsonutil.py +++ b/contributions/catalog/utils/jsonutil.py @@ -91,6 +91,23 @@ def create_capability_json_from_contribution_json(injson): return out_json_list +""" +create json for submitted from contribution list +""" +def create_status_json_from_contribution_json(injson, keyward): + out_json_list = [] + + # add capability first + for contribution in injson: + try: + if contribution["status"].lower() == keyward: + # need to add contribution id in capability as well] + out_json_list.append(contribution) + except: + logging.warning("There is no status in the contribution") + + return out_json_list + """ create json for talents for home page """ diff --git a/contributions/catalog/utils/requestutil.py b/contributions/catalog/utils/requestutil.py index 006b7101..530d51a2 100644 --- a/contributions/catalog/utils/requestutil.py +++ b/contributions/catalog/utils/requestutil.py @@ -48,11 +48,20 @@ def get_header_using_auth_token(auth_token): 'Authorization': 'Bearer ' + auth_token } +""" +request single contribution +""" """ request contribution list """ +def request_single_contribution(headers, contribution_id): + request_url = cfg.CONTRIBUTION_BUILDING_BLOCK_URL + "/" + contribution_id + result = requests.get(request_url, headers=headers) + + return result + """ -request reviewer +request contribution list """ def request_contributions(headers): result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL, headers=headers) diff --git a/contributions/catalog/webapps/static/css/style.css b/contributions/catalog/webapps/static/css/style.css index 6533395a..ca2ec426 100644 --- a/contributions/catalog/webapps/static/css/style.css +++ b/contributions/catalog/webapps/static/css/style.css @@ -77,6 +77,24 @@ input[type=submit]:hover { /*color: white;*/ } +input[type=reset] { + background-color: #6b9ec0; + color: #090000; + padding: 15px 25px; + text-align: center; + margin: 5px 5px; + transition-duration: 0.2s; + border: none; + border-radius: 4px; + cursor: pointer; + float: right; +} + +input[type=reset]:hover { + background-color: #378bc4; + /*color: white;*/ +} + /*input:hover {*/ /* color: white;*/ /*}*/ diff --git a/contributions/catalog/webapps/templates/contribute/capability_details.html b/contributions/catalog/webapps/templates/contribute/capability_details.html index a34dcbeb..72e074f4 100644 --- a/contributions/catalog/webapps/templates/contribute/capability_details.html +++ b/contributions/catalog/webapps/templates/contribute/capability_details.html @@ -45,24 +45,19 @@

Is code open source?

{{post.isOpenSource}}

- {% if post.isOpenSource == true %}
-

Api Documentation URL, if the above data is "Yes"

+

Source code repository URL

-

{{post.apiDocUrl}}

+

{{post.sourceRepoUrl}}

- {% endif %} - {% if post.sourceUrl %}
-

Source code repository URL

+

Api Documentation URL

-

{{post.sourceUrl}}

+

{{post.apiDocUrl}}

- {% endif %} - {% if post.deploymentDetails%}
diff --git a/contributions/catalog/webapps/templates/contribute/contribute.html b/contributions/catalog/webapps/templates/contribute/contribute.html index fd5940a2..b6014a0e 100644 --- a/contributions/catalog/webapps/templates/contribute/contribute.html +++ b/contributions/catalog/webapps/templates/contribute/contribute.html @@ -49,7 +49,9 @@ border: grey; border-radius: 5%; } - + #sourceurl{ + display:none; + } @@ -682,39 +684,79 @@

Add a Capability to this Contribution

- + {% else %} + name="capability_isOpenSource" type="radio" value="y"> yes {% endif %} - {% if capability | filter_nested_dict(["isOpensource"]) == False %} + {% if capability | filter_nested_dict(["isOpenSource"]) == False %} {% else %} {% endif %} {% else %} + name="capability_isOpenSource" type="radio" value="y"> yes {% endif %}
+ {% if is_editable %} + {% if capability | filter_nested_dict(["isOpenSource"]) == True %} +
+ +
+ +
+
+ {% elif capability | filter_nested_dict(["isOpenSource"]) == False %} + + {% endif %} + {% else %} + {% if capability | filter_nested_dict(["isOpenSource"]) == True %} +
+ +
+ +
+
+ {% elif capability | filter_nested_dict(["isOpenSource"]) == False %} + + {% endif %} + {% endif %} +
@@ -728,20 +770,6 @@

Add a Capability to this Contribution

{% endif %}
-
- -
- {# TODO what is source repo url?#} - {% if is_editable %} - - {% else %} - - {% endif %} -
-
-
- + +
+
- + +
- +
- {# TODO what is source repo url?#} - +
@@ -1105,35 +1134,47 @@

Add a Capability to this Contribution

- - -{% include 'topnav_javascript.html' %} - - \ No newline at end of file + diff --git a/contributions/catalog/webapps/templates/contribute/contribution_details.html b/contributions/catalog/webapps/templates/contribute/contribution_details.html index b93d69ff..e3b51c58 100644 --- a/contributions/catalog/webapps/templates/contribute/contribution_details.html +++ b/contributions/catalog/webapps/templates/contribute/contribution_details.html @@ -118,7 +118,11 @@

Capabilities in this contribution

    {{c.name}}

- + {% if is_review %} + + {% else %} + + {% endif %}
{% endfor %} {% endif %} @@ -130,12 +134,16 @@

Talents in this contribution

    {{t.name}}

- + {% if is_review %} + + {% else %} + + {% endif %} {% endfor %} {% endif %} - {% if reviewer %} + {% if (is_reviewer or is_editable or is_review) %}

Contribution admins

@@ -166,16 +174,104 @@

Date modified

{% endif %} - {% if reviewer %} + {% if is_editable %} Edit Contribution {% endif %} -
- - -
+ {% if is_reviewer %} + Review Contribution + {% endif %}
+{% if is_review %} +
+

Review

+
+
+
+
+
+

Status

+
+
+ +
+
+
+
+

Comment

+
+
+ {% if 'review' in post %} + + {% else %} + + {% endif %} +
+
+
+
+

Last Updated

+
+
+ {% if 'review' in post %} + {{ post | filter_nested_dict(["review", "lastUpdated"]) }} + {% endif %} +
+ +
+
+
+ + + + + +
+ + + +
+
+
+
+
+
+{% endif %} + {% include 'topnav_javascript.html' %} + + \ No newline at end of file diff --git a/contributions/catalog/webapps/templates/contribute/talent_details.html b/contributions/catalog/webapps/templates/contribute/talent_details.html index 9523ebc8..c7cf668a 100644 --- a/contributions/catalog/webapps/templates/contribute/talent_details.html +++ b/contributions/catalog/webapps/templates/contribute/talent_details.html @@ -72,10 +72,10 @@

Required Capabilities

{{x['isOpenSource']}} {% endif %} - {% if x['apiBaseUrl'] %} + {% if x['sourceRepoUrl'] %} Source code repository URL   - {{x['apiBaseUrl']}} + {{x['sourceRepoUrl']}} {% endif %} {% if x['apiDocUrl'] %} diff --git a/contributions/catalog/webapps/templates/head.html b/contributions/catalog/webapps/templates/head.html new file mode 100644 index 00000000..a4c2b3fd --- /dev/null +++ b/contributions/catalog/webapps/templates/head.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/contributions/catalog/webapps/templates/style.html b/contributions/catalog/webapps/templates/style.html new file mode 100644 index 00000000..6c49e7dd --- /dev/null +++ b/contributions/catalog/webapps/templates/style.html @@ -0,0 +1,156 @@ + \ No newline at end of file diff --git a/contributions/contribution.yaml b/contributions/contribution.yaml index c40c842d..305845b0 100644 --- a/contributions/contribution.yaml +++ b/contributions/contribution.yaml @@ -555,6 +555,11 @@ components: - Approved - Disapproved - Published + reviews: + items: + $ref: '#/components/schemas/Review' + nullable: true + type: array dateCreated: type: string format: date-time @@ -587,6 +592,10 @@ components: nullable: true type: string description: API documentation URL + sourceRepoUrl: + nullable: true + type: string + description: Source Repository documentation URL deploymentDetails: type: object properties: @@ -786,11 +795,28 @@ components: required: - name - githubUsername + - email properties: name: type: string githubUsername: type: string + email: + type: string + format: email + pattern: "^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+$" + Review: + description: A review from a reviewer. + properties: + reviewerId: + description: Reviewer's ID. + type: string + format: uuid + comment: + type: string + lastUpdated: + format: date-time + type: string securitySchemes: ApiKeyAuth: type: apiKey diff --git a/eventservice/README.md b/eventservice/README.md index 1591c840..eabb2e6c 100644 --- a/eventservice/README.md +++ b/eventservice/README.md @@ -318,6 +318,13 @@ This query supports main categories search and main/sub categories search. The r /events?category=Athletics.Football&category=Community ``` + +### CreatedBy Search +This query search on the `createdBy` field. The request can give multiple `createdBy` query parameters to get a list of returned events. +``` +/events?createdBy=user1@email.com&createdBy=user2@email.com +``` + ### Super Event Search When this query parameter is set to the ID of a super event, the endpoint will return all events as a list (including all the details) that are marked as a sub event of this super event. ``` diff --git a/eventservice/api/controllers/events.py b/eventservice/api/controllers/events.py index 9028b8ac..c33ffa65 100644 --- a/eventservice/api/controllers/events.py +++ b/eventservice/api/controllers/events.py @@ -30,13 +30,15 @@ from time import gmtime import controllers.configs as cfg +import controllers.messages as msgs from utils.db import get_db from utils import query_params from controllers.images.s3 import S3EventsImages from controllers.images import localfile from utils.cache import memoize , memoize_query, CACHE_GET_EVENTS, CACHE_GET_EVENT, CACHE_GET_EVENTIMAGES, CACHE_GET_CATEGORIES -from utils.group_auth import get_group_ids, get_group_memberships, check_group_event_admin_access, check_permission_access_event +from utils.group_auth import get_group_ids, get_group_memberships, check_group_event_admin_access, check_permission_access_event, \ + check_all_group_event_admin logging.Formatter.converter = gmtime logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%dT%H:%M:%S', @@ -47,16 +49,33 @@ def search(): group_ids = list() include_private_events = False + + is_all_group_event = False + + # check permission if the user has access to all group events. + try: + is_all_group_event = check_all_group_event_admin() + except Exception as ex: + msg = "Failed to parse the id token." + __logger.exception(msg, ex) + abort(500) + try: include_private_events, group_ids = get_group_ids() + # check if the user is in all group then give some boolean checking + # use the field called createdByGroupId to check if it is group event except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP) abort(500) args = request.args query = dict() try: - query = query_params.format_query(args, query, include_private_events, group_ids) + # if all group group then give the query with all the group event + if is_all_group_event: + query = query_params.format_query(args, query, True, None, True) + else: + query = query_params.format_query(args, query, include_private_events, group_ids) except Exception as ex: __logger.exception(ex) abort(400) @@ -67,7 +86,7 @@ def search(): args.get('skip', 0, int) ) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) __logger.info("[GET]: %s nRecords = %d ", request.url, result_len) return current_app.response_class(result, mimetype='application/json') @@ -153,7 +172,7 @@ def tags_search(): with open(tags_path, 'r') as tags_file: response = json.load(tags_file) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_SEARCH_TAG) abort(500) return flask.jsonify(response) @@ -165,7 +184,7 @@ def super_events_tags_search(): with open(tags_path, 'r') as tags_file: response = json.load(tags_file) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_SEARCH_SUPERTAG) abort(500) return flask.jsonify(response) @@ -175,7 +194,7 @@ def categories_search(): try: result, result_len = _get_categories_result() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_SEARCH_CATEGORY) abort(500) __logger.info("[GET]: %s nRecords = %d ", request.url, result_len) @@ -208,13 +227,13 @@ def get(event_id): try: include_private_events, group_ids = get_group_ids() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP) abort(500) try: result, result_found = _get_event_result({'_id': ObjectId(event_id)}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) if not result_found: @@ -270,7 +289,7 @@ def post(): msg_json = jsonutils.create_log_json("Events", "POST", {}) __logger.info("POST " + json.dumps(msg_json)) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_POST_EVENT) abort(500) return success_response(201, msg, str(event_id)) @@ -290,14 +309,14 @@ def put(event_id): req_data = query_params.formate_location(req_data) req_data = query_params.formate_category(req_data) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_UPDATE) abort(400) group_memberships = list() try: _, group_memberships = get_group_memberships() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) db = None event = None @@ -305,7 +324,7 @@ def put(event_id): db = get_db() event = db['events'].find_one({'_id': ObjectId(event_id)}, {'_id': 0}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) # If this is a group event, apply group authorization. Regular events can proceed like before. @@ -321,7 +340,7 @@ def put(event_id): msg_json = jsonutils.create_log_json("Events", "PUT", {}) __logger.info("PUT " + json.dumps(msg_json)) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_UPDATE) abort(500) return success_response(200, msg, str(event_id)) @@ -348,11 +367,12 @@ def patch(event_id): for data_tuple in db['events'].find({'_id': ObjectId(event_id)}, {'_id': 0, 'coordinates': 1}): coordinates = data_tuple.get('coordinates') if not coordinates: + __logger.exception(msgs.ERR_MSG_GET_COORDINATE) abort(500) break req_data = query_params.update_coordinates(req_data, coordinates) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_PATCH_EVENT) abort(500) except Exception as ex: @@ -363,7 +383,7 @@ def patch(event_id): try: _, group_memberships = get_group_memberships() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) db = None @@ -372,7 +392,7 @@ def patch(event_id): db = get_db() event = db['events'].find_one({'_id': ObjectId(event_id)}, {'_id': 0}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) if not check_group_event_admin_access(event, group_memberships): @@ -388,13 +408,26 @@ def patch(event_id): msg_json = jsonutils.create_log_json("Events", "PATCH", {}) __logger.info("PATCH " + json.dumps(msg_json)) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_PATCH_EVENT) abort(500) return success_response(200, msg, str(event_id)) def delete(event_id): - auth_middleware.authorize(auth_middleware.ROKWIRE_EVENT_WRITE_GROUPS) + can_delete = False + + # check permission if the user has access to all group events. + try: + is_all_group_event = check_all_group_event_admin() + if is_all_group_event: + can_delete = True + except Exception as ex: + msg = "Failed to parse the id token." + __logger.exception(msg, ex) + abort(500) + + if not can_delete: + auth_middleware.authorize(auth_middleware.ROKWIRE_EVENT_WRITE_GROUPS) if not ObjectId.is_valid(event_id): abort(400) @@ -403,7 +436,7 @@ def delete(event_id): try: _, group_memberships = get_group_memberships() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) db = None @@ -412,11 +445,12 @@ def delete(event_id): db = get_db() event = db['events'].find_one({'_id': ObjectId(event_id)}, {'_id': 0}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) - if not check_group_event_admin_access(event, group_memberships): - abort(401) + if not can_delete: + if not check_group_event_admin_access(event, group_memberships): + abort(401) try: db = get_db() @@ -429,7 +463,7 @@ def delete(event_id): msg_json = jsonutils.create_log_json("Events", "DELETE", {}) __logger.info("DELETE " + json.dumps(msg_json)) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_DELETE_EVENT) abort(500) if status.deleted_count == 0: abort(404) @@ -445,13 +479,13 @@ def images_search(event_id): try: include_private_events, group_ids = get_group_ids() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP) abort(500) try: result, result_found = _get_event_result({'_id': ObjectId(event_id)}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) if not result_found: @@ -464,7 +498,7 @@ def images_search(event_id): try: result = _get_imagefiles_result({'eventId': event_id}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_IMG_FILE) abort(500) msg = "[get images]: find %d images related to event %s" % (len(result), event_id) @@ -511,7 +545,7 @@ def images_post(event_id): else: raise except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_POST_IMG) abort(500) finally: localfile.deletefile(tmpfile) @@ -568,7 +602,7 @@ def images_put(event_id, image_id): try: _, group_memberships = get_group_memberships() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) db = None event = None @@ -576,7 +610,7 @@ def images_put(event_id, image_id): db = get_db() event = db['events'].find_one({'_id': ObjectId(event_id)}, {'_id': 0}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_EVENT) abort(500) # If this is a group event, apply group authorization. Regular events can proceed like before. @@ -604,7 +638,7 @@ def images_put(event_id, image_id): else: raise except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_IMG) abort(500) finally: localfile.deletefile(tmpfile) @@ -619,7 +653,7 @@ def images_delete(event_id, image_id): try: _, group_memberships = get_group_memberships() except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) db = None @@ -628,7 +662,7 @@ def images_delete(event_id, image_id): db = get_db() event = db['events'].find_one({'_id': ObjectId(event_id)}, {'_id': 0}) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_GET_GROUP_MEMBERSHIP) abort(500) if not check_group_event_admin_access(event, group_memberships): @@ -642,7 +676,7 @@ def images_delete(event_id, image_id): '_id': ObjectId(image_id) }) except Exception as ex: - __logger.exception(ex) + __logger.exception(msgs.ERR_MSG_DELETE_IMG) abort(500) return success_response(202, msg, str(event_id)) diff --git a/eventservice/api/controllers/messages.py b/eventservice/api/controllers/messages.py new file mode 100644 index 00000000..909b68ba --- /dev/null +++ b/eventservice/api/controllers/messages.py @@ -0,0 +1,30 @@ +# Copyright 2020 Board of Trustees of the University of Illinois. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +ERR_MSG_GET_GROUP_MEMBERSHIP = "failed to verify group membership." +ERR_MSG_GET_EVENT = "failed to get event." +ERR_MSG_GET_IMG = "failed to check existing image." +ERR_MSG_GET_GROUP = "failed to get group." +ERR_MSG_GET_IMG_FILE = "failed to get image." +ERR_MSG_GET_COORDINATE = "failed to get coordinate." +ERR_MSG_UPDATE = "event update failed." +ERR_MSG_PATCH_EVENT = "failed to patch the event." +ERR_MSG_DELETE_EVENT = "failed to delete event." +ERR_MSG_DELETE_IMG = "failed to delete image." +ERR_MSG_SEARCH_TAG = "tag search failed." +ERR_MSG_SEARCH_SUPERTAG = "super event tag search failed." +ERR_MSG_SEARCH_CATEGORY = "category search failed." +ERR_MSG_POST_EVENT = "event creation failed." +ERR_MSG_POST_IMG = "failed to post image." \ No newline at end of file diff --git a/eventservice/api/tags.json b/eventservice/api/tags.json index 9023523b..231afa48 100644 --- a/eventservice/api/tags.json +++ b/eventservice/api/tags.json @@ -1 +1 @@ -["20th century", "57N", "5k", "AASHO", "academic", "academic success", "access", "active learning", "activism", "administrative", "agriculture", "agronomy", "Allen Hall", "Allerton Park", "alumni", "american art", "animal studies", "animals", "application process", "art", "art and music", "art history", "arts", "arts & music", "Arts in the Ike", "Arts with US", "Ashton Woods", "asian", "Asian American Student Housing Organization", "asian art", "athletics", "awards ceremony", "Babcock Hall", "bands", "Barton Hall", "Beckwith", "Beckwith Residential Support Services", "bicycling", "big data", "biochemistry", "biography", "biological sciences", "Blaisdell Hall", "Block Party", "bluegrass", "book signing", "Bousfield Hall", "Break Housing", "breathing", "BRSS", "Busey Hall", "Busey-Evans", "Business LLC", "campus", "campus history", "Campus Recreation", "career development", "career fair", "careers", "Carr Hall", "CBSU", "celebration", "ceramics", "Chicago", "children", "china", "chinese art", "chinese ceramics", "Chomps", "choreography", "Clark Hall", "classical", "communications", "community", "community colleges", "community event", "completion", "concert", "construction", "contest", "conversation", "convocation", "CORE", "Cotton Club", "Counseling Center", "craft", "crafts", "cultural", "culture", "dance", "design", "development", "Dia de las Madres", "Dial-a-Carol", "disability studies", "discussion", "diversity & inclusion", "doctoral dissertation defense", "dorm", "Drag Show", "dutch art", "Ebony Emoja", "employment", "EMS", "Engaged Citizenship", "entrepreneurship", "environment", "equity", "european art", "Eusa Nia", "Evans Hall", "Ewezo", "exhibit", "expo", "faculty meeting", "faculty recognition", "family", "family fun", "FAR", "farm", "Fashion Show", "film", "fitness", "food", "free", "fun", "fundraiser", "games", "gardening", "gender", "genomics", "Global Crossroads", "global trade", "Goodwin Green", "Gospel Explosion", "grade entry", "grades", "Graduate and Upper Division Halls", "graduate students", "group fitness class", "gsirw", "GUD Halls", "Hall Closing", "Hall Opening", "health", "Health Professions", "healthy eating", "healthy food", "history", "homecoming", "Honors LLC", "Hopkins Hall", "Housing Sign Up", "human rights", "humanities", "iceskating", "iGuide", "iGuides", "Ike", "Ike North", "Ike South", "Ikenberry Commons", "Illini Guides", "Illini Union", "illinois", "Illinois Residential Experience", "Illinois Residential Leadership Experience", "implicit bias", "information session", "innovation", "innovation expo", "Innovation LLC", "international", "Interpersonal Competence", "Intersections", "investiture", "ISR", "japan", "japanese culture", "korea", "labor", "LAR", "Latino Student Association", "latino studies", "Leadership Suite", "LEADS", "learning", "lecture", "Leonard Hall", "live music", "LSA", "lunch", "lunch period", "lunchtime", "Ma'at", "Mariama", "MAs", "McKinley Health Center", "Meal Plan", "Men of Impact", "mentoring", "mhrir", "microaggressions", "mindfulness", "Moms' Day Brunch", "Move In", "MTD", "Multicultural Advocates", "museum", "music", "music practice rooms", "musical theatre", "national career development month", "National Residence Hall Honorary", "natural history", "nature", "networking", "no cost", "NRHH", "Nugent Hall", "nutrition", "Oglesby Hall", "oncology", "open house", "Orchard Downs", "outdoors", "outreach", "painting", "panel discussion", "PAR", "Parking", "Penn Station", "performance", "performance art", "persistence", "ph.d. advisory committee", "physics", "political science", "politics", "presentation", "prevention", "professional development", "proficiency exam", "public meeting", "publishing", "puzzles", "Queer Housing Coalition", "queer identity", "race", "RAs", "reading group", "recreation", "regional dynamics in northeast asia", "relaxation", "research", "research abroad", "research strategies", "research workshop", "Residence Hall Libraries", "Resident Advisors", "risk management", "rodeo", "roommate", "RSO", "run/walk", "running", "Salongo", "Saunders Hall", "scholarship", "scholarships", "school nutrition", "science", "science booth at farmers market", "Scott Hall", "SDRP", "SDRP", "self-care", "service", "Sheldon Hall", "Snyder Hall", "social", "social identity", "social justice", "Social Justice Exploration", "spirituality", "sports history", "startups", "stem", "stress management", "student activism", "student support", "students", "study abroad", "summer activities", "support", "sustainability", "Sustainability", "Taft Hall", "teaching & learning", "theater", "thesis defense", "town hall meeting", "Townsend Hall", "Trelease Hall", "TVD", "undergraduate research", "undergraduate research symposium", "undergraduate research week", "undergraduates and recent alumni", "Unit One", "University Housing", "Urbana North", "Urbana South", "urs", "urw", "VanDoren Hall", "Variety Show", "volunteering", "walking", "Wardall Hall", "Wassaja Hall", "webcast", "webinar", "welcome", "wellness", "Wellness", "Weston Exploration", "Weston Hall", "WEX", "WIMSE", "wine tasting", "women's history", "workshop", "Xfinity", "yoga"] \ No newline at end of file +["20th century", "57N", "5k", "AASHO", "academic", "academic success", "access", "active learning", "activism", "administrative", "agriculture", "agronomy", "Allen Hall", "Allerton Park", "alumni", "american art", "animal studies", "animals", "application process", "aquatics", "art", "art and music", "art history", "arts", "arts & music", "Arts in the Ike", "Arts with US", "Ashton Woods", "asian", "Asian American Student Housing Organization", "asian art", "athletics", "awards ceremony", "Babcock Hall", "bands", "Barton Hall", "Beckwith", "Beckwith Residential Support Services", "bicycling", "big data", "biochemistry", "biography", "biological sciences", "Blaisdell Hall", "Block Party", "bluegrass", "book signing", "Bousfield Hall", "Break Housing", "breathing", "BRSS", "Busey Hall", "Busey-Evans", "Business LLC", "campus", "campus history", "Campus Recreation", "career development", "career fair", "careers", "Carr Hall", "CBSU", "celebration", "ceramics", "Chicago", "children", "china", "chinese art", "chinese ceramics", "Chomps", "choreography", "Clark Hall", "classical", "communications", "community", "community colleges", "community event", "completion", "concert", "construction", "contest", "conversation", "convocation", "CORE", "Cotton Club", "Counseling Center", "craft", "crafts", "cultural", "culture", "dance", "design", "development", "Dia de las Madres", "Dial-a-Carol", "disability studies", "discussion", "diversity & inclusion", "doctoral dissertation defense", "dorm", "Drag Show", "dutch art", "Ebony Emoja", "employment", "EMS", "Engaged Citizenship", "entrepreneurship", "environment", "equity", "european art", "Eusa Nia", "Evans Hall", "Ewezo", "exhibit", "expo", "faculty meeting", "faculty recognition", "family", "family fun", "FAR", "farm", "Fashion Show", "film", "fitness", "food", "free", "fun", "fundraiser", "games", "gardening", "gender", "genomics", "Global Crossroads", "global trade", "Goodwin Green", "Gospel Explosion", "grade entry", "grades", "Graduate and Upper Division Halls", "graduate students", "group fitness class", "gsirw", "GUD Halls", "Hall Closing", "Hall Opening", "health", "Health Professions", "healthy eating", "healthy food", "history", "homecoming", "Honors LLC", "Hopkins Hall", "Housing Sign Up", "human rights", "humanities", "iceskating", "iGuide", "iGuides", "Ike", "Ike North", "Ike South", "Ikenberry Commons", "Illini Guides", "Illini Union", "illinois", "Illinois Residential Experience", "Illinois Residential Leadership Experience", "implicit bias", "information session", "innovation", "innovation expo", "Innovation LLC", "international", "Interpersonal Competence", "Intersections", "intramural", "investiture", "ISR", "japan", "japanese culture", "korea", "labor", "LAR", "Latino Student Association", "latino studies", "Leadership Suite", "LEADS", "learning", "lecture", "Leonard Hall", "live music", "LSA", "lunch", "lunch period", "lunchtime", "Ma'at", "Mariama", "MAs", "McKinley Health Center", "Meal Plan", "Men of Impact", "mentoring", "mhrir", "microaggressions", "mindfulness", "Moms' Day Brunch", "Move In", "MTD", "Multicultural Advocates", "museum", "music", "music practice rooms", "musical theatre", "national career development month", "National Residence Hall Honorary", "natural history", "nature", "networking", "no cost", "NRHH", "Nugent Hall", "nutrition", "Oglesby Hall", "oncology", "open house", "Orchard Downs", "outdoors", "outreach", "painting", "panel discussion", "PAR", "Parking", "Penn Station", "performance", "performance art", "persistence", "ph.d. advisory committee", "physics", "political science", "politics", "presentation", "prevention", "professional development", "proficiency exam", "public meeting", "publishing", "puzzles", "Queer Housing Coalition", "queer identity", "race", "RAs", "reading group", "recreation", "regional dynamics in northeast asia", "relaxation", "research", "research abroad", "research strategies", "research workshop", "Residence Hall Libraries", "Resident Advisors", "risk management", "rodeo", "roommate", "RSO", "run/walk", "running", "Salongo", "Saunders Hall", "scholarship", "scholarships", "school nutrition", "science", "science booth at farmers market", "Scott Hall", "SDRP", "SDRP", "self-care", "service", "Sheldon Hall", "Snyder Hall", "social", "social identity", "social justice", "Social Justice Exploration", "spirituality", "sports history", "startups", "stem", "stress management", "student activism", "student support", "students", "study abroad", "summer activities", "support", "sustainability", "Sustainability", "Taft Hall", "teaching & learning", "theater", "thesis defense", "town hall meeting", "Townsend Hall", "Trelease Hall", "TVD", "undergraduate research", "undergraduate research symposium", "undergraduate research week", "undergraduates and recent alumni", "Unit One", "University Housing", "Urbana North", "Urbana South", "urs", "urw", "VanDoren Hall", "Variety Show", "volunteering", "walking", "Wardall Hall", "Wassaja Hall", "webcast", "webinar", "welcome", "wellness", "Wellness", "Weston Exploration", "Weston Hall", "WEX", "WIMSE", "wine tasting", "women's history", "workshop", "Xfinity", "yoga"] diff --git a/eventservice/api/utils/group_auth.py b/eventservice/api/utils/group_auth.py index ca0c4f81..cb1b8cd8 100644 --- a/eventservice/api/utils/group_auth.py +++ b/eventservice/api/utils/group_auth.py @@ -4,6 +4,7 @@ from flask import g logger = logging.getLogger(__name__) +ALL_GROUP_EVENTS = 'all_group-events' def generate_groups_request(): @@ -80,4 +81,14 @@ def check_permission_access_event(event, include_private_events, group_ids): # check public group if event and event.get('isGroupPrivate') is True: return False - return True \ No newline at end of file + return True + + +def check_all_group_event_admin(): + is_all_group_event = False + if 'user_token' in g and not g.user_token_data.get('anonymous'): + if g.user_token_data.get('permissions'): + if g.user_token_data.get('permissions').lower().find(ALL_GROUP_EVENTS) != -1: + is_all_group_event = True + + return is_all_group_event diff --git a/eventservice/api/utils/query_params.py b/eventservice/api/utils/query_params.py index 7d62224c..a4cbacfc 100644 --- a/eventservice/api/utils/query_params.py +++ b/eventservice/api/utils/query_params.py @@ -17,7 +17,7 @@ from bson import ObjectId -def format_query(args, query, include_private_events=False, group_ids=None): +def format_query(args, query, include_private_events=False, group_ids=None, all_group_event=False): query_parts = [] # group id group_id = args.get('groupId') @@ -28,6 +28,8 @@ def format_query(args, query, include_private_events=False, group_ids=None): if super_event_id and ObjectId.is_valid(super_event_id): query_parts.append({'_id': ObjectId(super_event_id)}) query_parts.append({'isSuperEvent': True}) + if args.getlist('createdBy'): + query_parts.append({'createdBy': {'$in': args.getlist('createdBy')}}) # multiple events ids if args.getlist('id'): ids = list() @@ -155,15 +157,22 @@ def format_query(args, query, include_private_events=False, group_ids=None): # ApiKeyAuth query if not include_private_events: - query_parts.append({'isGroupPrivate': {'$ne': True}}) + if not all_group_event: + query_parts.append({'isGroupPrivate': {'$ne': True}}) query['$and'] = query_parts # UserAuth query else: - pubic_private_groups_access_parts = {'$or': [ - {'isGroupPrivate': {'$ne': True}}, # Includes both group public and regular public events - {'createdByGroupId': {'$in': group_ids}}]} # Check for group membership. Also include group private events. - query_parts.append(pubic_private_groups_access_parts) + if not all_group_event: + pubic_private_groups_access_parts = {'$or': [ + {'isGroupPrivate': {'$ne': True}}, # Includes both group public and regular public events + {'createdByGroupId': {'$in': group_ids}}]} # Check for group membership. Also include group private events. + query_parts.append(pubic_private_groups_access_parts) query['$and'] = query_parts + + # All_group_events + if all_group_event: + query_parts.append({'createdByGroupId': {'$exists': True}}) + return query diff --git a/eventservice/events.yaml b/eventservice/events.yaml index 08be2ea4..2b5201df 100755 --- a/eventservice/events.yaml +++ b/eventservice/events.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Rokwire Events Building Block API description: Events Building Block API Documentation - version: 1.12.1 + version: 1.13.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -202,6 +202,14 @@ paths: schema: type: string format: uuid + - name: createdBy + in: query + description: The parameter for searching user events based on the createdBy field. + required: false + style: form + explode: true + schema: + type: string responses: 200: description: search results matching criteria @@ -788,7 +796,7 @@ components: - alumni - parents - public - imageUrl: + imageURL: type: string icalUrl: type: string @@ -942,4 +950,4 @@ components: scheme: bearer bearerFormat: JWT x-bearerInfoFunc: auth_middleware.verify_userauth_coretoken - description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header including anonymous tokens \ No newline at end of file + description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header including anonymous tokens diff --git a/eventservice/requirements.txt b/eventservice/requirements.txt index e7cb3d8f..24969461 100644 --- a/eventservice/requirements.txt +++ b/eventservice/requirements.txt @@ -1,4 +1,6 @@ Flask==1.1.1 +itsdangerous==2.0.1 +Jinja2==3.0.3 pymongo[tls,srv]==3.7.2 gunicorn==20.0.4 boto3==1.9.188 diff --git a/profileservice/api/controllers/profiles.py b/profileservice/api/controllers/profiles.py index 6730f890..17ce9dd4 100644 --- a/profileservice/api/controllers/profiles.py +++ b/profileservice/api/controllers/profiles.py @@ -13,16 +13,15 @@ # limitations under the License. import json -import datetime +import hmac import logging import uuid as uuidlib import copy -from flask import jsonify, request, g +from flask import request, g from bson import ObjectId import controllers.configs as cfg -import utils.mongoutils as mongoutils import utils.jsonutils as jsonutils import utils.datasetutils as datasetutils import utils.rest_handlers as rs_handlers @@ -235,7 +234,7 @@ def get_data_list(uuid): return None, None, True, resp def core_search(uin=None, phone=None): - if request.headers.get("ROKWIRE-CORE-BB-API-KEY") != cfg.ROKWIRE_CORE_BB_API_KEY: + if not hmac.compare_digest(request.headers.get("ROKWIRE-CORE-BB-API-KEY"), cfg.ROKWIRE_CORE_BB_API_KEY): msg = { "reason": "Unauthorized", "error": "Unauthorized: " + request.url, diff --git a/profileservice/api/utils/tokenutils.py b/profileservice/api/utils/tokenutils.py index 6fe21d16..86808dc3 100644 --- a/profileservice/api/utils/tokenutils.py +++ b/profileservice/api/utils/tokenutils.py @@ -17,20 +17,12 @@ def get_id_info_from_token(in_token): id_type = 0 # 0 for no pii, 1 for uin id, 2 phone id id_string = "" - # check if the pii token is from campus or from outside the campus # if there is uin, it is from campus if 'uiucedu_uin' in in_token: id_string = in_token['uiucedu_uin'] id_type = 1 - # TODO following lines are modified to use email instead of uin in id checking - # for GET, POST, and DELETE - # # if there is email, it is from campus - # if 'email' in in_token: - # id_string = in_token['email'] - # id_type = 1 - # if there is phone number, it is from outside the campus if 'phoneNumber' in in_token: id_string = in_token['phoneNumber'] @@ -39,16 +31,6 @@ def get_id_info_from_token(in_token): return id_type, id_string -def convert_str_to_dict(in_token): - while True: - try: - converted_dict = eval(in_token) - except NameError as e: - key = e.message.split("'")[1] - in_token = in_token.replace(key, "'{}'".format(key)) - else: - return converted_dict - def get_data_from_token(in_token): tk_uin = in_token.get('uiucedu_uin') tk_is_uin = tk_uin is not None