diff --git a/.github/workflows/detect-secrets.yaml b/.github/workflows/detect-secrets.yaml index b52b8434..ab471b79 100644 --- a/.github/workflows/detect-secrets.yaml +++ b/.github/workflows/detect-secrets.yaml @@ -14,7 +14,7 @@ jobs: run: | apt-get update && apt-get install -y jq pip install yq - pip install detect-secrets==$(yq -r .[0].rev .pre-commit-config.yaml) + pip install detect-secrets==$(yq -r .repos[0].rev .pre-commit-config.yaml) - name: Detect potential secrets run: find -type f -not -path './.git/*' -printf '%P\n' | xargs detect-secrets-hook --baseline .secrets.baseline diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7361a1b6..919e6409 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ +repos: - repo: git@github.com:Yelp/detect-secrets - rev: v0.14.1 + rev: v1.0.3 hooks: - id: detect-secrets args: ['--baseline', '.secrets.baseline'] diff --git a/.secrets.baseline b/.secrets.baseline index 886afc9a..a27b7e7b 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1,20 +1,18 @@ { - "custom_plugin_paths": [], - "exclude": { - "files": null, - "lines": null - }, - "generated_at": "2020-08-05T02:59:28Z", + "version": "1.0.3", "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, { "name": "AWSKeyDetector" }, { - "name": "ArtifactoryDetector" + "name": "AzureStorageKeyDetector" }, { - "base64_limit": 4.5, - "name": "Base64HighEntropyString" + "name": "Base64HighEntropyString", + "limit": 4.5 }, { "name": "BasicAuthDetector" @@ -23,8 +21,8 @@ "name": "CloudantDetector" }, { - "hex_limit": 3, - "name": "HexHighEntropyString" + "name": "HexHighEntropyString", + "limit": 3.0 }, { "name": "IbmCloudIamDetector" @@ -36,12 +34,15 @@ "name": "JwtTokenDetector" }, { - "keyword_exclude": null, - "name": "KeywordDetector" + "name": "KeywordDetector", + "keyword_exclude": "" }, { "name": "MailchimpDetector" }, + { + "name": "NpmDetector" + }, { "name": "PrivateKeyDetector" }, @@ -51,6 +52,9 @@ { "name": "SoftlayerDetector" }, + { + "name": "SquareOAuthDetector" + }, { "name": "StripeDetector" }, @@ -58,134 +62,166 @@ "name": "TwilioKeyDetector" } ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], "results": { "appconfigservice/README.md": [ { - "hashed_secret": "0dd91d7d2dee491919369a6d60e20954ba22ea7c", - "is_secret": false, + "type": "Secret Keyword", + "filename": "appconfigservice/README.md", + "hashed_secret": "055d8a847dfea9c1c840140dd0940b0a5a832565", "is_verified": false, "line_number": 22, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", - "is_secret": false, + "type": "Secret Keyword", + "filename": "appconfigservice/README.md", + "hashed_secret": "c2d2d8a23ffdcdee0e074b02d3a076d1546dc803", "is_verified": false, "line_number": 28, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "a91dbe46987cdf705256b5833997728c9add55f6", - "is_secret": false, + "type": "Secret Keyword", + "filename": "appconfigservice/README.md", + "hashed_secret": "93b6e269c00e67237d474cb068bbfb1362ea48b8", "is_verified": false, "line_number": 52, - "type": "Secret Keyword" - } - ], - "appconfigservice/appconfig-v010.json": [ - { - "hashed_secret": "a523e602e6a14449f8a8c3a4a2a2d00525eeb556", - "is_secret": false, - "is_verified": false, - "line_number": 20, - "type": "Basic Auth Credentials" - } - ], - "appconfigservice/appconfig-v094.json": [ - { - "hashed_secret": "a523e602e6a14449f8a8c3a4a2a2d00525eeb556", - "is_secret": false, - "is_verified": false, - "line_number": 27, - "type": "Basic Auth Credentials" + "is_secret": false } ], "appconfigservice/appconfig.yaml": [ { + "type": "Hex High Entropy String", + "filename": "appconfigservice/appconfig.yaml", "hashed_secret": "a45a360e6ff9bceaf3fcfef370a6d6e1d4ba9271", - "is_secret": false, "is_verified": false, "line_number": 65, - "type": "Hex High Entropy String" + "is_secret": false } ], "auth-middleware-test-svc/README.md": [ { - "hashed_secret": "0dd91d7d2dee491919369a6d60e20954ba22ea7c", - "is_secret": false, + "type": "Secret Keyword", + "filename": "auth-middleware-test-svc/README.md", + "hashed_secret": "055d8a847dfea9c1c840140dd0940b0a5a832565", "is_verified": false, "line_number": 30, - "type": "Secret Keyword" + "is_secret": false } ], "authservice/README.md": [ { - "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", - "is_secret": false, + "type": "Secret Keyword", + "filename": "authservice/README.md", + "hashed_secret": "c2d2d8a23ffdcdee0e074b02d3a076d1546dc803", "is_verified": false, "line_number": 73, - "type": "Secret Keyword" + "is_secret": false + } + ], + "contributions/catalog/Dockerfile": [ + { + "type": "Secret Keyword", + "filename": "contributions/catalog/Dockerfile", + "hashed_secret": "b6ee60926c0a426addcbb7e087d4274498f35b1c", + "is_verified": false, + "line_number": 23, + "is_secret": false } ], "eventservice/README.md": [ { - "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", - "is_secret": false, + "type": "Secret Keyword", + "filename": "eventservice/README.md", + "hashed_secret": "c2d2d8a23ffdcdee0e074b02d3a076d1546dc803", "is_verified": false, "line_number": 24, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "289a8d69fc17eb0668911e4a2c2f43f409c1b6e0", - "is_secret": false, + "type": "Secret Keyword", + "filename": "eventservice/README.md", + "hashed_secret": "99f11f7789a5315285e569895445507118f188bd", "is_verified": false, "line_number": 30, - "type": "Secret Keyword" + "is_secret": false } ], "loggingservice/README.md": [ { - "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", - "is_secret": false, + "type": "Secret Keyword", + "filename": "loggingservice/README.md", + "hashed_secret": "c2d2d8a23ffdcdee0e074b02d3a076d1546dc803", "is_verified": false, "line_number": 22, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "289a8d69fc17eb0668911e4a2c2f43f409c1b6e0", - "is_secret": false, + "type": "Secret Keyword", + "filename": "loggingservice/README.md", + "hashed_secret": "99f11f7789a5315285e569895445507118f188bd", "is_verified": false, "line_number": 28, - "type": "Secret Keyword" + "is_secret": false } ], "profileservice/README.md": [ { - "hashed_secret": "0dd91d7d2dee491919369a6d60e20954ba22ea7c", - "is_secret": false, + "type": "Secret Keyword", + "filename": "profileservice/README.md", + "hashed_secret": "055d8a847dfea9c1c840140dd0940b0a5a832565", "is_verified": false, "line_number": 38, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "81b127e2222d9bfc4609053faec85300f7525463", - "is_secret": false, + "type": "Secret Keyword", + "filename": "profileservice/README.md", + "hashed_secret": "c2d2d8a23ffdcdee0e074b02d3a076d1546dc803", "is_verified": false, "line_number": 44, - "type": "Secret Keyword" + "is_secret": false }, { - "hashed_secret": "289a8d69fc17eb0668911e4a2c2f43f409c1b6e0", - "is_secret": false, + "type": "Secret Keyword", + "filename": "profileservice/README.md", + "hashed_secret": "99f11f7789a5315285e569895445507118f188bd", "is_verified": false, "line_number": 50, - "type": "Secret Keyword" + "is_secret": false } ] }, - "version": "0.14.1", - "word_list": { - "file": null, - "hash": null - } + "generated_at": "2021-11-17T22:48:48Z" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 66086b75..9ab636a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - More information to logs in event services when performing POST, PUT, DELETE. [#822](https://github.com/rokwire/rokwire-building-blocks-api/issues/822) +## [1.12.0] - 2021-11-19 +### Added +- Add edit contribution capability in catalog. [#737](https://github.com/rokwire/rokwire-building-blocks-api/issues/737) +- Add edit contribution button based on the logged in user. [#766](https://github.com/rokwire/rokwire-building-blocks-api/issues/766) +- Add contributionAdmins to capability and talent detail end point. [#730](https://github.com/rokwire/rokwire-building-blocks-api/issues/730) +- Catalog release script. [#784](https://github.com/rokwire/rokwire-building-blocks-api/issues/784) +- /ok endpoint for healthcheck in AppConfig.[#754](https://github.com/rokwire/rokwire-building-blocks-api/issues/754) +- /version endpoint in AppConfig.[#760](https://github.com/rokwire/rokwire-building-blocks-api/issues/760) +- Core BB Auth Support [#808](https://github.com/rokwire/rokwire-building-blocks-api/issues/808) +- Core BB profile migration [#809](https://github.com/rokwire/rokwire-building-blocks-api/issues/809) +- SECURITY.md file. [#804](https://github.com/rokwire/rokwire-building-blocks-api/issues/804) +- Add version endpoint in Events building block.[#725](https://github.com/rokwire/rokwire-building-blocks-api/issues/725) + +### Changed +- Updated pre-commit config and corresponding GitHub Action to use `Yelp/detect-secrets` version 1.0.3. [#649](https://github.com/rokwire/rokwire-building-blocks-api/issues/649) +- Make catalog pages to show all the items for contribution admins. [#728](https://github.com/rokwire/rokwire-building-blocks-api/issues/728) +- Make contribution details pages to show all items for reviewers and admins. [#728](https://github.com/rokwire/rokwire-building-blocks-api/issues/728) +- Catalog default port and default URL prefix. [#784](https://github.com/rokwire/rokwire-building-blocks-api/issues/784) +- Modified contribution schema for required capability [#790](https://github.com/rokwire/rokwire-building-blocks-api/issues/790) +- Default Contributions Catalog port to 5000 and made it configurable. [#788](https://github.com/rokwire/rokwire-building-blocks-api/issues/788) + +### Fixed +- Fix get event endpoint with user auth key. [#746](https://github.com/rokwire/rokwire-building-blocks-api/issues/746) +- Fix published capabilities and talents not showing without login. [#767](https://github.com/rokwire/rokwire-building-blocks-api/issues/767) +- Fix catalog use python generated result instead of java script. [#772](https://github.com/rokwire/rokwire-building-blocks-api/issues/772) +- Contribution POST gives proper error message. [#714](https://github.com/rokwire/rokwire-building-blocks-api/issues/714) +- Hard coded base url. [#780](https://github.com/rokwire/rokwire-building-blocks-api/issues/780) +- Multiple bugs/improvements around login. [#782](https://github.com/rokwire/rokwire-building-blocks-api/issues/782) +- Fix login problem with session. [#796](https://github.com/rokwire/rokwire-building-blocks-api/issues/796) +- Fix logout. [#775](https://github.com/rokwire/rokwire-building-blocks-api/issues/775) +- Fix top navigation to provide correct login info. [#773](https://github.com/rokwire/rokwire-building-blocks-api/issues/773) +- Fix login button provide correct info when user not logged in. [#778](https://github.com/rokwire/rokwire-building-blocks-api/issues/778) +- Contributions Catalog release script and Dockerfile. [#787](https://github.com/rokwire/rokwire-building-blocks-api/issues/787) +- Added version and gitsha in catalog footer [#770](https://github.com/rokwire/rokwire-building-blocks-api/issues/770) +- Auth middleware lib to recognize Rokwire issuer. [#823](https://github.com/rokwire/rokwire-building-blocks-api/issues/823) +- Groups membership request in Events BB. [#823](https://github.com/rokwire/rokwire-building-blocks-api/issues/823) + +## [1.11.3] - 2021-11-18 +### Added +- More information to logs in event services when performing POST, PUT, DELETE. [#822](https://github.com/rokwire/rokwire-building-blocks-api/issues/822) + ## [1.11.2] - 2021-09-17 ### Changed - Update Events Building Block logs and related configuration. [#793](https://github.com/rokwire/rokwire-building-blocks-api/issues/793) @@ -374,7 +415,10 @@ 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.11.1...HEAD +[Unreleased]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.12.0...HEAD +[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 +[1.11.2]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.11.1...1.11.2 [1.11.1]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.11.0...1.11.1 [1.11.0]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.10.0...1.11.0 [1.10.0]: https://github.com/rokwire/rokwire-building-blocks-api/compare/1.9.1...1.10.0 diff --git a/README.md b/README.md index ebe4fd82..80b27764 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ API definitions of the Rokwire Platform building blocks ## Install commit hooks ``` -$ pip install detect-secrets pre-commit +$ pip install -r requirements-dev.txt $ pre-commit install ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f750f39b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +Patches for **Rokwire Building Blocks** in this repository will only be applied to the following versions: + +| Version | Supported | +| ------- | ------------------ | +| 1.12.0 | :white_check_mark: | +| 1.11.3 | :white_check_mark: | +| 1.11.2 | :white_check_mark: | +| 1.11.1 | :white_check_mark: | +| 1.11.0 | :white_check_mark: | +| 1.10.0 | :white_check_mark: | +| < 1.10.0 | :x: | + +## Reporting a Vulnerability + +Vulnerabilities can be responsibly disclosed to [securitysupport@illinois.edu](mailto:securitysupport@illinois.edu). +Bugs can be reported by creating a [GitHub issue](https://github.com/rokwire/rokwire-building-blocks-api/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D). diff --git a/appconfigservice/Dockerfile b/appconfigservice/Dockerfile index deda03e3..e8de7bba 100644 --- a/appconfigservice/Dockerfile +++ b/appconfigservice/Dockerfile @@ -1,12 +1,13 @@ FROM python:3-alpine3.12 -LABEL maintainer="wzhu26@illinois.edu" +LABEL maintainer="minum@illinois.edu" WORKDIR /app COPY appconfigservice ./appconfigservice COPY lib ../lib/ COPY appconfigservice/appconfig.yaml . +COPY appconfigservice/appconfig.yaml /app/appconfigservice/api ENV APP_CONFIG_MONGO_URL="" ENV APP_CONFIG_URL_PREFIX="" diff --git a/appconfigservice/api/controllers/app.py b/appconfigservice/api/controllers/app.py index b12c262f..d7ae3591 100644 --- a/appconfigservice/api/controllers/app.py +++ b/appconfigservice/api/controllers/app.py @@ -15,6 +15,7 @@ import logging import re from time import gmtime +import yaml import auth_middleware import controllers.config as cfg @@ -34,6 +35,26 @@ app = flask.Flask(__name__) +def configs_ok_search(): + """ + Function for healthcheck public endpoint + Return : {'success':'true'} + """ + msg = {"success": "true"} + return msg + + +def configs_version_search(): + """ + Method to return AppConfig BB version number + Reads yaml file and returns version number + Return : plain text - version number + """ + appconfig_yaml = open('appconfig.yaml') + parsed_appconfig_yaml = yaml.load(appconfig_yaml, Loader=yaml.FullLoader) + return parsed_appconfig_yaml['info']['version'] + + def configs_search(mobileAppVersion=None): args = request.args version = args.get('mobileAppVersion') diff --git a/appconfigservice/appconfig.yaml b/appconfigservice/appconfig.yaml index c4c8ae88..e24cdab8 100755 --- a/appconfigservice/appconfig.yaml +++ b/appconfigservice/appconfig.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Rokwire App Config Building Block API description: App Config Building Block API Documentation - version: 1.11.0 + version: 1.12.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -25,7 +25,7 @@ paths: ```urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire app config manager``` security: - - UserAuth: [] + - CoreUserAuth: [] requestBody: description: Creates AppConfig object content: @@ -91,6 +91,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: mobileAppVersion in: query @@ -116,6 +117,40 @@ paths: description: AppConfig not found 500: description: Internal error + /app/configs/ok: + get: + tags: + - App Configuration + summary: Healthcheck endpoint + description: Public endpoint which returns success reponse + responses: + 200: + description: Healthcheck ok + 400: + description: Bad request. + 404: + description: AppConfig not found + 500: + description: Internal error + /app/configs/version: + get: + tags: + - App Configuration + summary: version endpoint + description: Public endpoint which returns the latest version of appconfig BB + responses: + 200: + description: version returned as text + content: + text/plain: + schema: + type: string + 400: + description: Bad request. + 404: + description: AppConfig not found + 500: + description: Internal error /app/configs/{id}: get: tags: @@ -125,6 +160,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: id in: path @@ -160,7 +196,7 @@ paths: ```urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire app config manager``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: id in: path @@ -199,7 +235,7 @@ paths: ```urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire app config manager``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: id in: path @@ -263,9 +299,15 @@ components: name: ROKWIRE-API-KEY x-apikeyInfoFunc: auth_middleware.verify_apikey description: Each client version has unique API key (e.g., "c6befa22-50a6-4403-a8fc-378c9719743b"). For API endpoints that do not require user authentication, the ROKWIRE-API-KEY header must contain an API key corresponding to a supported client. - UserAuth: + CoreUserAuth: type: http scheme: bearer bearerFormat: JWT # https://openid.net/specs/openid-connect-core-1_0.html [id_token] - x-bearerInfoFunc: auth_middleware.verify_userauth + x-bearerInfoFunc: auth_middleware.verify_core_userauth description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header when making requests to API endpoints that require user authentication. + CoreTokenAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: auth_middleware.verify_core_token + 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 diff --git a/appconfigservice/requirements.txt b/appconfigservice/requirements.txt index 48338018..914ac5a7 100644 --- a/appconfigservice/requirements.txt +++ b/appconfigservice/requirements.txt @@ -8,7 +8,7 @@ gunicorn==20.0.4 python-dotenv==0.10.3 gevent==20.9.0 diskcache==4.0.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 ../lib/auth-middleware diff --git a/auth-middleware-test-svc/requirements.txt b/auth-middleware-test-svc/requirements.txt index b700b422..87363ba3 100644 --- a/auth-middleware-test-svc/requirements.txt +++ b/auth-middleware-test-svc/requirements.txt @@ -1,6 +1,6 @@ Flask==1.1.1 gunicorn==20.0.4 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 python-dotenv==0.10.3 ../lib/auth-middleware diff --git a/authservice/auth.yaml b/authservice/auth.yaml index d87d44b4..a5b8f930 100644 --- a/authservice/auth.yaml +++ b/authservice/auth.yaml @@ -2,7 +2,7 @@ openapi: 3.0.2 info: title: Rokwire Auth Building Block API description: Authentication Building Block API Documentation - version: "1.11.0" + version: 1.12.0 paths: /authentication/phone-initiate: post: diff --git a/authservice/requirements.txt b/authservice/requirements.txt index 049124c0..bc33e84e 100644 --- a/authservice/requirements.txt +++ b/authservice/requirements.txt @@ -1,6 +1,6 @@ Flask>=1.0.3,<1.1 schema==0.7.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 requests==2.22.0 pyjwt==1.7.1 gunicorn==20.0.4 diff --git a/contributions/api/controllers/contributions.py b/contributions/api/controllers/contributions.py index fc844381..46d94303 100644 --- a/contributions/api/controllers/contributions.py +++ b/contributions/api/controllers/contributions.py @@ -172,7 +172,7 @@ def post(token_info): if talent.requiredCapabilities is not None: required_cap_list = talent.requiredCapabilities for capability_json in required_cap_list: - capability, rest_capability_json, msg = modelutils.construct_capability(capability_json) + capability, rest_capability_json, msg = modelutils.construct_required_capability(capability_json) is_uuid = otherutils.check_if_uuid(capability.id) if not is_uuid: msg = { @@ -532,9 +532,12 @@ def capabilities_search(token_info=None, id=None): contribution_dataset, status_code = mongoutils.get_contribution_dataset_from_objectid( coll_contribution, id, login_id, is_login) + if status_code == '200': if contribution_dataset is not None: capability_dataset = contribution_dataset.get_capabilities() + for capability in capability_dataset: + capability["contributionAdmins"] = contribution_dataset.contributionAdmins else: msg = { "reason": "There is no contribution dataset with given id, " @@ -580,7 +583,7 @@ def capabilities_search(token_info=None, id=None): return rs_handlers.not_found(msg_json) msg = { - "search": "Capability data in the contirubion dataset with given id : " + str(id) + "search": "Capability data in the contribution dataset with given id : " + str(id) } msg_json = jsonutils.create_log_json("Capability", "GET", msg) logging.info("Capability GET " + json.dumps(msg)) @@ -688,6 +691,8 @@ def talents_search(token_info=None, id=None): if status_code == '200': if contribution_dataset is not None: talent_dataset = contribution_dataset.get_talents() + for talent in talent_dataset: + talent["contributionAdmins"] = contribution_dataset.contributionAdmins else: msg = { "reason": "There is no contribution dataset with given id, " @@ -733,7 +738,7 @@ def talents_search(token_info=None, id=None): return rs_handlers.not_found(msg_json) msg = { - "search": "Talent data in the contirubion dataset with given id : " + str(id) + "search": "Talent data in the contribution dataset with given id : " + str(id) } msg_json = jsonutils.create_log_json("Talent", "GET", msg) logging.info("Talent GET " + json.dumps(msg)) @@ -786,7 +791,7 @@ def capabilities_get(token_info=None, id=None, capability_id=None): def talents_get(token_info=None, id=None, talent_id=None): talent_datasets = talents_search(token_info, id) - # when the capablity_datasets is an error response + # when the talent_datasets is an error response if isinstance(talent_datasets, wrappers.Response): return talent_datasets diff --git a/contributions/api/models/capabilities/capability.py b/contributions/api/models/capabilities/capability.py index 1f6fbbf4..24a89bb9 100644 --- a/contributions/api/models/capabilities/capability.py +++ b/contributions/api/models/capabilities/capability.py @@ -24,9 +24,9 @@ def __init__(self, injson): self.deploymentDetails = None self.apiBaseUrl = None self.version = None + self.versionUrl = None self.healthCheckUrl = None self.dataDeletionEndpointDetails = None - self.contacts = None # self.creationDate = None # self.lastModifiedDate = None @@ -92,6 +92,12 @@ def set_version(self, version): def get_version(self): return self.version + def set_version_url(self, versionUrl): + self.versionUrl = versionUrl + + def get_version_url(self): + return self.versionUrl + def set_health_check_url(self, healthCheckUrl): self.healthCheckUrl = healthCheckUrl @@ -110,12 +116,6 @@ def set_data_deletion_endpoint_details(self, dataDeletionEndpointDetails): def get_data_deletion_endpoint_details(self): return self.dataDeletionEndpointDetails - def set_contacts(self, contacts): - self.contacts = contacts - - def get_contacts(self): - return self.contacts - # def set_creation_date(self, creationDate): # self.creationDate = creationDate # diff --git a/contributions/api/models/capabilities/contact.py b/contributions/api/models/contact.py similarity index 100% rename from contributions/api/models/capabilities/contact.py rename to contributions/api/models/contact.py diff --git a/contributions/api/models/contribution.py b/contributions/api/models/contribution.py index 17bf78b5..7d4750cf 100644 --- a/contributions/api/models/contribution.py +++ b/contributions/api/models/contribution.py @@ -21,6 +21,7 @@ def __init__(self, injson): self.shortDescription = None self.longDescription = None self.contributors = None + self.contacts = None self.capabilities = None self.talents = None self.status = None @@ -77,6 +78,12 @@ def set_contributors(self, contributors): def get_contributors(self): return self.contributors + def set_contacts(self, contacts): + self.contacts = contacts + + def get_contacts(self): + return self.contacts + def set_date_created(self, dateCreated): self.dateCreated = dateCreated diff --git a/contributions/api/models/talents/requiredcapability.py b/contributions/api/models/talents/requiredcapability.py new file mode 100644 index 00000000..4e3ddab3 --- /dev/null +++ b/contributions/api/models/talents/requiredcapability.py @@ -0,0 +1,37 @@ +# Copyright 2021 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. + +class RequiredCapability: + def __init__(self): + self.contributionId = None + self.capabilityId = None + self.capabilityName = None + + def set_contribution_id(self, contributionId): + self.contributionId = contributionId + + def get_contribution_id(self): + return self.contributionId + + def set_capability_id(self, capabilityId): + self.capabilityId = capabilityId + + def get_capability_id(self): + return self.capabilityId + + def set_capability_name(self, capabilityName): + self.capabilityName = capabilityName + + def get_capability_name(self): + return self.capabilityName diff --git a/contributions/api/utils/datasetutils.py b/contributions/api/utils/datasetutils.py index 50bfa769..33f5bfa6 100644 --- a/contributions/api/utils/datasetutils.py +++ b/contributions/api/utils/datasetutils.py @@ -14,11 +14,12 @@ import copy -from models.capabilities.contact import Contact +from models.contact import Contact from models.capabilities.datadeletionendpointdetail import DataDeletionEndpointDetail from models.capabilities.deploymentdetails import DeploymentDetails from models.capabilities.environmentvariable import EnvironmentVariable from models.talents.selfcertification import SelfCertification +from models.talents.requiredcapability import RequiredCapability """ update contribution dataset @@ -49,6 +50,44 @@ def update_contribution_dataset_from_json(dataset, injson): del outjson['contributors'] except: pass + try: + contact_list = [] + contact_json = injson['contacts'] + for i in range(len(contact_json)): + tmp_contact = Contact() + try: + name = injson["contacts"][i]["name"] + tmp_contact.set_name(name) + except: + pass + try: + email = injson["contacts"][i]["email"] + tmp_contact.set_email(email) + except: + pass + try: + phone = injson["contacts"][i]["phone"] + tmp_contact.set_phone(phone) + except: + pass + try: + organization = injson["contacts"][i]["organization"] + tmp_contact.set_organization(organization) + except: + pass + try: + officialAddress = injson["contacts"][i]["officialAddress"] + tmp_contact.set_officialAddress(officialAddress) + except: + pass + contact_list.append(tmp_contact) + dataset.set_contacts(contact_list) + try: + del outjson["contacts"] + except: + pass + except Exception as e: + pass try: dataset.set_capabilities(injson['capabilities']) del outjson['capabilities'] @@ -167,6 +206,11 @@ def update_capability_dataset_from_json(dataset, injson): del outjson['version'] except: pass + try: + dataset.set_version_url(injson['versionUrl']) + del outjson['versionUrl'] + except: + pass try: dataset.set_health_check_url(injson["healthCheckUrl"]) del outjson["healthCheckUrl"] @@ -201,44 +245,6 @@ def update_capability_dataset_from_json(dataset, injson): pass except Exception as e: pass - try: - contact_list = [] - contact_json = injson['contacts'] - for i in range(len(contact_json)): - tmp_contact = Contact() - try: - name = injson["contacts"]["name"] - tmp_contact.set_name(name) - except: - pass - try: - email = injson["contacts"]["email"] - tmp_contact.set_email(email) - except: - pass - try: - phone = injson["contacts"]["phone"] - tmp_contact.set_phone(phone) - except: - pass - try: - organization = injson["contacts"]["organization"] - tmp_contact.set_organization(organization) - except: - pass - try: - officialAddress = injson["contacts"]["officialAddress"] - tmp_contact.set_officialAddress(officialAddress) - except: - pass - contact_list.append(tmp_contact) - dataset.set_contacts(contact_list) - try: - del outjson["contacts"] - except: - pass - except Exception as e: - pass try: dataset.set_creation_date(injson["creationDate"]) del outjson["creationDate"] @@ -301,7 +307,7 @@ def update_organization_dataset_from_json(dataset, injson): except: pass try: - dataset.set_email(injson['address']) + dataset.set_address(injson['address']) del outjson['address'] except: pass @@ -344,13 +350,13 @@ def update_talent_dataset_from_json(dataset, injson): except: pass try: - capabilities_json = injson['requiredCapabilities'] - capablities_list = [] - for capability_json in capabilities_json: - capability_dataset = None - capability_dataset = update_capability_dataset_from_json(capability_dataset, capability_json) - if capability_dataset is not None: - capablities_list.append(capability_dataset) + required_capabilities_json = injson['requiredCapabilities'] + required_capabilities_list = [] + for required_capability_json in required_capabilities_json: + required_capability_dataset = None + required_capability_dataset = update_required_capability_dataset_from_json(required_capability_dataset, required_capability_json) + if required_capability_dataset is not None: + required_capabilities_list.append(required_capability_dataset) dataset.set_required_capabilities(injson['requiredCapabilities']) del outjson['requiredCapabilities'] except: @@ -422,6 +428,29 @@ def update_talent_dataset_from_json(dataset, injson): return dataset, outjson +""" +update required capability dataset +""" +def update_required_capability_dataset_from_json(dataset, injson): + outjson = copy.copy(injson) + try: + dataset.set_contribution_id(injson['contributionId']) + del outjson['contributionId'] + except: + pass + try: + dataset.set_capability_id(injson['capabilityId']) + del outjson['capabilityId'] + except: + pass + try: + dataset.set_capability_name(injson['capabilityName']) + del outjson['capabilityName'] + except: + pass + + return dataset, outjson + """ update reviewer dataset """ diff --git a/contributions/api/utils/modelutils.py b/contributions/api/utils/modelutils.py index aa2d1308..d07fb860 100644 --- a/contributions/api/utils/modelutils.py +++ b/contributions/api/utils/modelutils.py @@ -24,6 +24,7 @@ from models.organization import Organization from models.capabilities.capability import Capability from models.talents.talent import Talent +from models.talents.requiredcapability import RequiredCapability def construct_capability(in_json): @@ -56,6 +57,13 @@ def construct_talent(in_json): return talent_dataset, restjson, None +def construct_required_capability(in_json): + # new installation of the app + required_capability_dataset = RequiredCapability('') + required_capability_dataset, restjson = datasetutils.update_required_capability_dataset_from_json(required_capability_dataset, in_json) + + return required_capability_dataset, restjson, None + def construct_contributors(in_json): # need to know if it is person or organization # TODO make better algorithm for this, but for now use if there is firstname or lastname it is a Person, otherwise, organization @@ -71,6 +79,11 @@ def construct_contributors(in_json): is_person = True except: pass + try: + middlename = in_json["middleName"] + is_person = True + except: + pass contributor_dataset = None if is_person: diff --git a/contributions/api/utils/mongoutils.py b/contributions/api/utils/mongoutils.py index e293fde2..6d162c9c 100644 --- a/contributions/api/utils/mongoutils.py +++ b/contributions/api/utils/mongoutils.py @@ -223,19 +223,16 @@ def get_contribution_dataset_from_objectid(collection, objectid, login_id=None, json_load = jsonutils.convert_obejctid_from_dataset_json(json_load) dataset = Contribution(json_load) - if (is_login): - # check if the user is in contributionAdmin group - if (is_admin): - return dataset, status_code - else: - status_code = '401' - return None, status_code + if status == "Published": + return dataset, status_code else: - if status != "Published": - status_code = '401' - return None, status_code - else: - return dataset, status_code + if (is_login): + # check if the user is in contributionAdmin group + if (is_admin): + return dataset, status_code + else: + status_code = '401' + return None, status_code else: status_code = '400' return None, status_code diff --git a/contributions/catalog/Dockerfile b/contributions/catalog/Dockerfile index b29d722e..206cc620 100644 --- a/contributions/catalog/Dockerfile +++ b/contributions/catalog/Dockerfile @@ -1,12 +1,10 @@ FROM python:3-alpine3.12 -LABEL maintainer=", " - -EXPOSE 5050 +LABEL maintainer=", " WORKDIR /usr/src/app/catalog -COPY catalog . +COPY contributions/catalog . RUN apk --update add python3 py3-pip openssl ca-certificates py3-openssl && \ apk --update add --virtual build-dependencies libffi-dev openssl-dev python3-dev py3-pip musl-dev cargo build-base && \ @@ -16,6 +14,9 @@ RUN apk --update add python3 py3-pip openssl ca-certificates py3-openssl && \ WORKDIR /usr/src/app/catalog +ARG GIT_TAG +ARG GIT_SHA + ENV CONTRIBUTION_BUILDING_BLOCK_URL='' \ DB_NAME='' \ GITHUB_CLIENT_ID='' \ @@ -23,7 +24,12 @@ ENV CONTRIBUTION_BUILDING_BLOCK_URL='' \ AUTHORIZATION_BASE_URL='https://github.com/login/oauth/authorize' \ TOKEN_URL='https://github.com/login/oauth/access_token' \ AUTHENTICATION_TOKEN='' \ - URL_PREFIX='' \ - DEBUG='False' + URL_PREFIX='/catalog' \ + DEBUG='False' \ + CATALOG_PORT=5000 \ + GIT_TAG=$GIT_TAG \ + GIT_SHA=$GIT_SHA + +EXPOSE ${CATALOG_PORT} CMD ["gunicorn", "catalog_rest_service:app", "--config", "/usr/src/app/catalog/gunicorn.config.py"] \ No newline at end of file diff --git a/contributions/catalog/README.md b/contributions/catalog/README.md index 861ee5d2..ac5a6d95 100644 --- a/contributions/catalog/README.md +++ b/contributions/catalog/README.md @@ -17,12 +17,25 @@ **Configuration** -The necessary configuration should be configured in configure file (configs.py) that is located under profileservice folder. This contains the MongoDB url, database name, collection name and so on. Modify the information in the file appropriately. +All configurations can be found in the configuration file (configs.py) that is located under the `controllers` folder. ## Environment File You need to have a `.env` file in this directory that contains credentials required for authentication. -Not all of these variables may be required for this building block. +Other configuration items can also be added to the `.env` file. + +Example .env file: + +```shell +ROKWIRE_API_KEY= # Rokwire API key. +FLASK_ENV= # Flask runtime environment. +DEBUG=True # Boolean to enable/disable debug mode. +GITHUB_CLIENT_ID= # GitHub OAuth App client ID. +GITHUB_CLIENT_SECRET= # pragma: allowlist secret. GitHub OAuth App client secret. +SECRET_KEY= # A secret key that will be used for securely signing the session cookies. +ADMIN_USERS= +CATALOG_PORT= # Port to run the Contributions Catalog (default: 5000). +``` ## Run application @@ -56,3 +69,26 @@ set FLASK_ENV=development python catalog_rest_service.py ``` If you want to use gunicorn, cd into api folder then, use ` gunicorn catalog_rest_service:app -c gunicorn.config.py` instead of `python catalog_rest_service.py` + + +### Run using Docker + +``` +cd rokwire-building-blocks-api +docker build --pull -f contributions/catalog/Dockerfile -t rokwire/contributions-catalog . +docker run --name catalog --rm --env-file=contributions/catalog/.env -e MONGO_URL=mongodb://:27017 -p 5050:5000 rokwire/contributions-catalog + +For a list of available configurations, please see `controllers/config.py`. + + +### AWS ECR Instructions + +Make sure that a repository called rokwire/contributions-catalog exists in ECR. Then, create the Docker image for Contributions Catalog and push to AWS ECR using: + +``` +cd rokwire-building-blocks-api +aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 779619664536.dkr.ecr.us-east-2.amazonaws.com +docker build --pull -f contributions/catalog/Dockerfile -t rokwire/contributions-catalog . +docker tag rokwire/contributions-catalog:latest 779619664536.dkr.ecr.us-east-2.amazonaws.com/rokwire/contributions-catalog:latest +docker push 779619664536.dkr.ecr.us-east-2.amazonaws.com/rokwire/contributions-catalog:latest +``` \ No newline at end of file diff --git a/contributions/catalog/catalog_rest_service.py b/contributions/catalog/catalog_rest_service.py index 3131595c..dfd9c2f8 100644 --- a/contributions/catalog/catalog_rest_service.py +++ b/contributions/catalog/catalog_rest_service.py @@ -1,10 +1,27 @@ +# Copyright 2021 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 logging import os -from time import gmtime +import utils.requestutil as requestutil +import utils.jsonutil as jsonutil +from time import gmtime +from jinja2 import environment from flask import Flask, redirect, url_for, render_template, request, session from requests_oauthlib import OAuth2Session -from controllers.auth import login_required + from controllers.config import Config as cfg from controllers.contribute import bp as contribute_bp from db import init_app @@ -37,10 +54,79 @@ app.register_blueprint(contribute_bp) +@app.template_filter('filter_nested_dict') +def filter_nested_dict(dict, item_list): + try: + for item in item_list: + dict = dict[item] + except: + return '' + + return dict + +environment.DEFAULT_FILTERS['filter_nested_dict'] = filter_nested_dict + + @app.route("/", methods=["GET"]) def index(): - return render_template('contribute/home.html') - + show_sel = request.args.get('show') + is_logged_in = False + cap_json = [] + tal_json = [] + user = None + + if 'GIT_TAG' in os.environ: + git_tag=os.environ['GIT_TAG'] + else: + git_tag='' + if 'GIT_SHA' in os.environ: + git_sha=os.environ['GIT_SHA'] + else: + git_sha='' + + 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): + # query with auth + headers = requestutil.get_header_using_session(session) + result = requestutil.request_contributions(headers) + if show_sel == "capability": + # create the json for only capability + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + elif show_sel == "talent": + # create the json for only talent + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + else: + # create the json for only capability and talent + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + else: + # query only published ones + headers = requestutil.get_header_using_api_key() + result = requestutil.request_contributions(headers) + if show_sel == "capability": + # create the json for only capability + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + elif show_sel == "talent": + # create the json for only talent + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + else: + # create the json for only capability and talent + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + + return render_template('contribute/home.html', git_tag=git_tag, git_sha=git_sha, + cap_json=cap_json, tal_json=tal_json, show_sel=show_sel, user=user) @app.route("/login") def login(): @@ -57,32 +143,26 @@ def login(): # Step 2: User authorization, this happens on the provider. -# "http://localhost:5050/contributions/catalog/auth/callback" @app.route("/contributions/catalog/auth/callback", methods=["GET"]) def callback(): """ Step 3: Retrieving an access token. """ # print("Step 3: Retrieving an access token") - github = OAuth2Session(cfg.GITHUB_CLIENT_ID, state=session['oauth_state']) + if 'oauth_state' in session: + github = OAuth2Session(cfg.GITHUB_CLIENT_ID, state=session['oauth_state']) + else: + github = OAuth2Session(cfg.GITHUB_CLIENT_ID, state=None) token = github.fetch_token(cfg.TOKEN_URL, client_secret=cfg.GITHUB_CLIENT_SECRET, authorization_response=request.url) session['oauth_token'] = token - return redirect(url_for('.profile')) - -@app.route("/contribute/profile", methods=["GET"]) -def profile(): - """Fetching a protected resource using an OAuth 2 token. - Parsing the username to the seesion dict, to the templates to display. - """ - # print("Fetching a protected resource using an OAuth 2 token") + # Retrieve basic user info github = OAuth2Session(cfg.GITHUB_CLIENT_ID, token=session['oauth_token']) - resp = github.get('https://api.github.com/user') + resp = github.get(cfg.USER_INFO_URL) session["username"] = resp.json()["login"] session['name'] = resp.json()["name"] - return render_template('contribute/home.html', user=session["name"], token=session['oauth_token']['access_token']) - + return redirect(url_for('contribute.home')) if __name__ == '__main__': - app.run(port=5050, host=None, debug=True) + app.run(port=cfg.CATALOG_PORT, host=None, debug=True) diff --git a/contributions/catalog/controllers/config.py b/contributions/catalog/controllers/config.py index dc3ab2b2..6362333e 100644 --- a/contributions/catalog/controllers/config.py +++ b/contributions/catalog/controllers/config.py @@ -27,7 +27,7 @@ class Config(object): MONGO_URL = os.getenv('MONGO_URL', 'mongodb://localhost:27017') DB_NAME = os.getenv("MONGO_DATABASE", "contribution") DB_COLLECTION = os.getenv("MONGO_DATABASE", "catalog") - URL_PREFIX = os.getenv("URL_PREFIX", "") + URL_PREFIX = os.getenv("URL_PREFIX", "/catalog") DBTYPE = 'mongoDB' SECRET_KEY = os.getenv("SECRET_KEY", "SECRET_KEY") AUTHENTICATION_TOKEN = os.getenv("AUTHENTICATION_TOKEN", "...") @@ -37,5 +37,7 @@ class Config(object): GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "NO SECRET") AUTHORIZATION_BASE_URL = os.getenv("AUTHORIZATION_BASE_URL", 'https://github.com/login/oauth/authorize') TOKEN_URL = os.getenv("TOKEN_URL", 'https://github.com/login/oauth/access_token') + USER_INFO_URL = os.getenv("USER_INFO_URL", 'https://api.github.com/user') CORS_ENABLED = bool(os.getenv('CORS_ENABLED', 'False') == 'True') ADMIN_USERS = os.getenv('ADMIN_USERS', '') + CATALOG_PORT = int(os.getenv('CATALOG_PORT', '5000')) diff --git a/contributions/catalog/controllers/contribute.py b/contributions/catalog/controllers/contribute.py index 13e6743a..21f6034b 100644 --- a/contributions/catalog/controllers/contribute.py +++ b/contributions/catalog/controllers/contribute.py @@ -13,14 +13,15 @@ # limitations under the License. import json +import logging import traceback import requests -import functools from flask import ( Blueprint, render_template, request, session, redirect, url_for ) from requests_oauthlib import OAuth2Session +from formencode import variabledecode from .auth import login_required from controllers.config import Config as cfg from models.contribution_utilities import to_contribution @@ -28,18 +29,82 @@ from utils import adminutil from utils import requestutil -bp = Blueprint('contribute', __name__, url_prefix='/contribute') +import os + +bp = Blueprint('contribute', __name__, url_prefix=cfg.URL_PREFIX) @bp.route('/', methods=['GET', 'POST']) def home(): - print("homepage.") - if request.method == 'POST' and request.validate_on_submit(): - result = request.form.to_dict(flat=False) - if "name" in session: - return render_template('contribute/home.html', user=session["name"], token=session['oauth_token']['access_token']) + show_sel = request.args.get('show') + user = None + token = None + is_logged_in = False + cap_json = [] + tal_json = [] + + if 'GIT_TAG' in os.environ: + git_tag=os.environ['GIT_TAG'] else: - return render_template('contribute/home.html') + git_tag='' + if 'GIT_SHA' in os.environ: + git_sha=os.environ['GIT_SHA'] + else: + git_sha='' + + 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): + # query with auth + headers = requestutil.get_header_using_session(session) + result = requestutil.request_contributions(headers) + if show_sel == "capability": + # create the json for only capability + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + elif show_sel == "talent": + # create the json for only talent + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + else: + # create the json for only capability and talent + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + + return render_template('contribute/home.html', git_tag=git_tag, git_sha=git_sha, cap_json=cap_json, tal_json=tal_json, + show_sel=show_sel, user=user) + else: + # query only published ones + headers = requestutil.get_header_using_api_key() + result = requestutil.request_contributions(headers) + if show_sel == "capability": + # create the json for only capability + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + elif show_sel == "talent": + # create the json for only talent + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + else: + # create the json for only capability and talent + cap_json = jsonutil.create_capability_json_from_contribution_json(result.json()) + tal_json = jsonutil.create_talent_json_from_contribution_json(result.json()) + + return render_template('contribute/home.html', git_tag=git_tag, git_sha=git_sha, cap_json=cap_json, tal_json=tal_json, show_sel=show_sel) + + # print("homepage.") + # if request.method == 'POST' and request.validate_on_submit(): + # result = request.form.to_dict(flat=False) + # if "name" in session: + # return render_template('contribute/home.html', user=session["name"], token=session['oauth_token']['access_token']) + # else: + # return render_template('contribute/home.html') @bp.route('/login', methods=['GET', 'POST']) @@ -58,7 +123,7 @@ def login(): @bp.route('/logout') def logout(): session.clear() - return render_template('contribute/home.html') + return redirect(url_for('.home')) @bp.route('/results', methods=['POST', 'GET']) @@ -71,64 +136,215 @@ def result(): search(query) return render_template("contribute/results.html", user=session["name"], token=session['oauth_token']['access_token'], result=result) -@bp.route('details/', methods=['GET']) +@bp.route('/contributions/', methods=['GET']) def contribution_details(contribution_id): - the_json_res = get_contribution(contribution_id) - return render_template("contribute/contribution_details.html", post=the_json_res, user=session["name"]) + # check if the user is logged in + is_logged_in = False -@bp.route('details//capabilities/', methods=['GET']) + try: + # create error to see if the use 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 + except: + is_logged_in = 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"] + 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 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) + +@bp.route('/contributions//edit', methods=['GET', 'POST']) +@login_required +def contribution_edit(contribution_id): + if request.method == 'POST': + is_put = False + contribution_id = None + result = request.form.to_dict(flat=False) + + # check if it is PUT + try: + 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: + contribution = to_contribution(result) + contribution = jsonutil.add_contribution_admins(contribution, is_edit=True) + # remove id from json_data + del contribution["id"] + json_contribution = json.dumps(contribution, indent=4) + response, s = put_contribution(json_contribution, contribution_id) + + if response: + if "name" in session: + return render_template('contribute/submitted.html', user=session["name"], + token=session['oauth_token']['access_token']) + 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 editable then set the is_editable + is_editable = False + username = session["username"] + headers = requestutil.get_header_using_session(session) + is_editable = adminutil.check_if_reviewer(username, headers) + + # 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, + 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//capabilities/', methods=['GET']) def capability_details(contribution_id, id): - the_json_res = get_capability(contribution_id, id) + # check if the user is logged in + is_logged_in = False - # check if the user is reviewer by requesting to endpoint - username = session["username"] - headers = requestutil.get_header_using_session(session) - is_reviewer = adminutil.check_if_reviewer(username, headers) + try: + # create error to see if the use 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 + except: + is_logged_in = False + + is_reviewer = False + name = "" + + if (is_logged_in): + the_json_res = get_capability(contribution_id, id) + is_contribution_admin = False + username = session["username"] + name = session["name"] + contribution_admins = the_json_res["contributionAdmins"] + if username in contribution_admins: + is_contribution_admin = True + if is_contribution_admin: + is_reviewer = True + else: + # check if the user is reviewer by requesting to endpoint + headers = requestutil.get_header_using_session(session) + is_reviewer = adminutil.check_if_reviewer(username, headers) + + return render_template("contribute/capability_details.html", reviewer=is_reviewer, post=the_json_res, user=name) + else: + the_json_res = get_capability_with_api_key(contribution_id, id) + return render_template("contribute/capability_details.html", reviewer=is_reviewer, post=the_json_res) - return render_template("contribute/capability_details.html", reviewer=is_reviewer, post=the_json_res, user=session["name"]) -@bp.route('details//talents/', methods=['GET']) +@bp.route('/contributions//talents/', methods=['GET']) def talent_details(contribution_id, id): - the_json_res = get_talent(contribution_id, id) + # check if the user is logged in + is_logged_in = False - # check if the user is reviewer by requesting to endpoint - username = session["username"] - headers = requestutil.get_header_using_session(session) - is_reviewer = adminutil.check_if_reviewer(username, headers) + try: + # create error to see if the use 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 + except: + is_logged_in = False + + is_reviewer = False + name = "" + + if (is_logged_in): + is_contribution_admin = False + the_json_res = get_talent(contribution_id, id) + username = session["username"] + name = session["name"] + contribution_admins = the_json_res["contributionAdmins"] + if username in contribution_admins: + is_contribution_admin = True + + if is_contribution_admin: + is_reviewer = True + else: + # check if the user is reviewer by requesting to endpoint + username = session["username"] + headers = requestutil.get_header_using_session(session) + is_reviewer = adminutil.check_if_reviewer(username, headers) - return render_template("contribute/talent_details.html", reviewer=is_reviewer, post=the_json_res, user=session["name"]) + return render_template("contribute/talent_details.html", reviewer=is_reviewer, post=the_json_res, user=name) + else: + the_json_res = get_talent_with_api_key(contribution_id, id) + + return render_template("contribute/talent_details.html", reviewer=is_reviewer, post=the_json_res) # @bp.route('/edit/', methods=('GET', 'POST')) # def edit(contribution_id): # #todo: need to implement the edit form page # return render_template("contribute/contribution_details.html", contribution_json=the_json_res) -@bp.route('/create', methods=['GET', "POST"]) +@bp.route('/contributions/create', methods=['GET', 'POST']) @login_required def create(): + json_contribute = None if request.method == 'POST': 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) - # add contributionAdmins to the json_contiubtion + # add contributionAdmins to the json_contribution contribution = jsonutil.add_contribution_admins(contribution) json_contribution = json.dumps(contribution, indent=4) - response, s = post(json_contribution) + response, s = post_contribution(json_contribution) if response: - if response: - if "name" in session: - return render_template('contribute/submitted.html', user=session["name"], token=session['oauth_token']['access_token']) - 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) - return render_template('contribute/contribute.html', user=session["name"], token=session['oauth_token']['access_token']) - + if "name" in session: + return render_template('contribute/submitted.html', user=session["name"], token=session['oauth_token']['access_token']) + else: + return render_template('contribute/submitted.html') + elif not response: + logging.error(s) + s = "Contribution submission failed. Please try again after some time!" + 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) + + # get capability list to create required capability list + header = requestutil.get_header_using_session(session) + required_capability_list = requestutil.request_required_capability_list(header) + + return render_template('contribute/contribute.html', required_capabilities=required_capability_list, + post=json_contribute, user=session["name"], token=session['oauth_token']['access_token']) @bp.errorhandler(404) def page_not_found(e): @@ -149,7 +365,7 @@ def search_results(search): # post a json_data in a http request -def post(json_data): +def post_contribution(json_data): headers = requestutil.get_header_using_session(session) try: # Setting up post request @@ -158,11 +374,13 @@ def post(json_data): data=json_data) if result.status_code != 200: - print("post method fails".format(json_data)) - print("with error code:", result.status_code) - return False, str("post method fails with error: ") + str(result.status_code) + err_json = parse_response_error(result) + logging.error("Contribution POST " + json.dumps(err_json)) + err_msg = str(err_json['status']), err_json['title'], err_json['detail'] + return False, str("post method fails with error: ") + str(result.status_code) \ + + ": " + str(err_msg) else: - print("posted ok.".format(json_data)) + logging.info("posted ok.".format(json_data)) return True, str("post success!") except Exception: @@ -170,10 +388,54 @@ def post(json_data): var = traceback.format_exc() return False, var +# PUT a json_data in a http request +def put_contribution(json_data, contribution_id): + headers = requestutil.get_header_using_session(session) + try: + # set PUT url + put_url = cfg.CONTRIBUTION_BUILDING_BLOCK_URL + "/" + contribution_id + + # Setting up post request + result = requests.put(put_url, + headers=headers, + data=json_data) + + if result.status_code != 200: + logging.error("PUT method failed. " + str(result.text)) + return False, str("Error Code: " + str(result.status_code) + + ". There was an error when editing your Contribution. Please try again later!") + else: + logging.info("PUT OK.".format(contribution_id)) + return True, str("Your Contribution has been successfully updated.") + + except Exception: + traceback.print_exc() + var = "There was an error when updating your Contribution. Please try again later!" + return False, var def get_contribution(contribution_id): headers = requestutil.get_header_using_session(session) + try: + if contribution_id: + result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL + "/" + str(contribution_id), + headers=headers) + + if result.status_code != 200: + err_json = parse_response_error(result) + logging.error("Contribution GET " + json.dumps(err_json)) + return {} + else: + print("GET ok.".format(contribution_id)) + + except Exception: + # traceback.print_exc() + return False + return result.json() + +def get_contribution_with_api_key(contribution_id): + headers = requestutil.get_header_using_api_key() + try: if contribution_id: result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL + "/" + str(contribution_id), @@ -194,6 +456,24 @@ def get_contribution(contribution_id): def get_capability(contribution_id, cid): headers = requestutil.get_header_using_session(session) + try: + if contribution_id and cid: + result = requestutil.request_capability(headers, contribution_id, cid) + if result.status_code != 200: + err_json = parse_response_error(result) + logging.error("Capability GET " + json.dumps(err_json)) + return {} + else: + print("GET ok.".format(id)) + + except Exception: + # traceback.print_exc() + return False + return result.json() + +def get_capability_with_api_key(contribution_id, cid): + headers = requestutil.get_header_using_api_key() + try: if contribution_id and cid: result = requestutil.request_capability(headers, contribution_id, cid) @@ -216,6 +496,24 @@ def get_talent(contribution_id, tid): if id: result = requestutil.request_talent(headers, contribution_id, tid) + if result.status_code != 200: + err_json = parse_response_error(result) + logging.error("Talent GET " + json.dumps(err_json)) + return {} + else: + print("GET ok.".format(id)) + + except Exception: + # traceback.print_exc() + return False + return result.json() + +def get_talent_with_api_key(contribution_id, tid): + headers = requestutil.get_header_using_api_key() + + try: + if contribution_id and tid: + result = requestutil.request_talent(headers, contribution_id, tid) if result.status_code != 200: print("GET method fails".format(id)) print("with error code:", result.status_code) @@ -242,8 +540,8 @@ def search(input_data): headers=headers) if result.status_code != 200: - print("post method fails".format(input_data)) - print("with error code:", result.status_code) + err_json = parse_response_error(result) + logging.error("Search " + json.dumps(err_json)) return False else: print("posted ok.".format(input_data)) @@ -252,3 +550,12 @@ def search(input_data): except Exception: # traceback.print_exc() return False + +""" +parse error response and convert to json object +""" +def parse_response_error(response): + err_content = response.content.decode("utf-8").replace('\n', '') + err_json = json.loads(err_content) + + return err_json diff --git a/contributions/catalog/gunicorn.config.py b/contributions/catalog/gunicorn.config.py index 3be66649..73273ffd 100644 --- a/contributions/catalog/gunicorn.config.py +++ b/contributions/catalog/gunicorn.config.py @@ -1,6 +1,8 @@ +import os + """Gunicorn configuration.""" -bind = '0.0.0.0:5000' +bind = '0.0.0.0:' + str(os.getenv('CATALOG_PORT', '5000')) workers = 4 worker_class = 'gevent' diff --git a/contributions/catalog/models/capability_utilities.py b/contributions/catalog/models/capability_utilities.py index 306222f7..18f44bfc 100644 --- a/contributions/catalog/models/capability_utilities.py +++ b/contributions/catalog/models/capability_utilities.py @@ -80,28 +80,5 @@ def to_capability(d): capability_list[i][name] = [] else: capability_list[i][name] = v[i] - capability_list[i]["contacts"] = to_contact(d) return capability_list - - -def init_contact(): - d = {"name": "", - "email": "", - "phone": "", - "organization": "", - "officialAddress": "" - } - return d - - -def to_contact(d): - if not d: return {} - res = [init_contact()] - for cont in res: - for k, v in d.items(): - if "contact_" in k: - name = k.split("contact_")[-1] - print(name, v) - cont[name] = v[0] - return res diff --git a/contributions/catalog/models/contribution_utilities.py b/contributions/catalog/models/contribution_utilities.py index 6392d650..71333929 100644 --- a/contributions/catalog/models/contribution_utilities.py +++ b/contributions/catalog/models/contribution_utilities.py @@ -23,6 +23,7 @@ def init_contribution(): "longDescription": '', 'contributionAdmins': [], "contributors": [], + "contacts": '', "capabilities": [], "talents": [] } @@ -95,6 +96,28 @@ def to_contributor(d): return person_list + org_list +def init_contact(): + d = {"name": "", + "email": "", + "phone": "", + "organization": "", + "officialAddress": "" + } + return d + + +def to_contact(d): + if not d: return {} + res = [init_contact()] + for cont in res: + for k, v in d.items(): + if "contact_" in k: + name = k.split("contact_")[-1] + print(name, v) + cont[name] = v[0] + return res + + def to_contribution(d): if not d: return {} res = init_contribution() @@ -107,6 +130,8 @@ def to_contribution(d): res["talents"] = talent contributor = to_contributor(d) res["contributors"] = contributor + contact = to_contact(d) + res["contacts"] = contact for k, v in d.items(): if "contribution_" in k: diff --git a/contributions/catalog/models/talent_utilities.py b/contributions/catalog/models/talent_utilities.py index c265dc9b..d2d55cb2 100644 --- a/contributions/catalog/models/talent_utilities.py +++ b/contributions/catalog/models/talent_utilities.py @@ -14,6 +14,7 @@ from datetime import date import uuid +import json def init_talent(): d = { @@ -27,14 +28,7 @@ def init_talent(): "minEndUserRoles": [], "startDate": date.today().strftime("%d/%m/%Y"), "endDate": date.today().strftime("%d/%m/%Y"), - "dataDescription": '', - "selfCertification": { - "dataDeletionUponRequest": '', - "respectingUserPrivacySetting": '', - "discloseAds": '', - "discloseSponsors": '', - "discloseImageRights": '' - } + "dataDescription": '' } return d @@ -61,10 +55,48 @@ def to_talent(d): 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: - talent_list[i][name].append(v[i]) + 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] = v[i] + talent_list[i]["selfCertification"] = to_self_certification(d) return talent_list + +def init_self_certification(): + d = { + "dataDeletionUponRequest": '', + "respectingUserPrivacySetting": '', + "discloseAds": '', + "discloseSponsors": '', + "discloseImageRights": '' + } + + return d + + +def to_self_certification(d): + if not d: return {} + res = init_self_certification() + for k, v in d.items(): + if "selfcertificate_" in k: + name = k.split("selfcertificate_")[-1] + res[name] = v[0] + return res + +def reconstruct_required_capabilities(d): + # removed \r\n + d = json.loads(d.replace("\r\n","")) + + return d \ No newline at end of file diff --git a/contributions/catalog/requirements.txt b/contributions/catalog/requirements.txt index 31e8961f..27809ca0 100644 --- a/contributions/catalog/requirements.txt +++ b/contributions/catalog/requirements.txt @@ -4,7 +4,5 @@ gunicorn==19.9.0 requests==2.22.0 requests-oauthlib==1.3.0 gevent==20.9.0 -<<<<<<< HEAD python-dotenv>=0.15.0 -======= ->>>>>>> master +FormEncode>=2.0.0 diff --git a/contributions/catalog/utils/adminutil.py b/contributions/catalog/utils/adminutil.py index 6b9cfc88..1e9fae13 100644 --- a/contributions/catalog/utils/adminutil.py +++ b/contributions/catalog/utils/adminutil.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + from utils import requestutil from controllers.config import Config as cfg @@ -40,9 +42,19 @@ def check_if_reviewer(login_id, headers): # otherwise it will give 401 result = requestutil.request_reviewers(headers) - # if you can get this result, the user is a reviewer + # if the result is not 201, it is not reviewer if result.status_code == 200: - return True + # check if the login id is in reviewer list + result_str = result.content.decode('utf-8').replace('\n', '') + reviewer_list = json.loads(result_str) + + # create the list with only the user name + reviewer_name_list = [] + for reviewer in reviewer_list: + reviewer_name_list.append(reviewer["githubUsername"]) + + if login_id in reviewer_name_list: + return True return False diff --git a/contributions/catalog/utils/jsonutil.py b/contributions/catalog/utils/jsonutil.py index 46d2bd83..a37dbaaf 100644 --- a/contributions/catalog/utils/jsonutil.py +++ b/contributions/catalog/utils/jsonutil.py @@ -11,13 +11,15 @@ # 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 logging + from bson import ObjectId from flask import session ''' add contributionAdmins element in the contribution json ''' -def add_contribution_admins(in_json): +def add_contribution_admins(in_json, is_edit=False): login_user = session['username'] admin_input = in_json['contributionAdmins'] @@ -26,7 +28,10 @@ def add_contribution_admins(in_json): if admin_input: contribution_admins = [admin_name for admin_name in admin_input.split(',') if admin_name] - updated_json = {"contributionAdmins": [login_user] + contribution_admins} + if is_edit: + updated_json = {"contributionAdmins": contribution_admins} + else: + updated_json = {"contributionAdmins": [login_user] + contribution_admins} in_json.update(updated_json) return in_json @@ -68,3 +73,38 @@ def remove_objectid_from_dataset(dataset): return dataset +""" +create json for capabilities for home page +""" +def create_capability_json_from_contribution_json(injson): + out_json_list = [] + + # add capability first + for contribution in injson: + try: + for capability in contribution["capabilities"]: + # need to add contribution id in capability as well + capability["cont_id"] = contribution["id"] + out_json_list.append(capability) + except: + logging.warning("There is no capability in the contribution") + + return out_json_list + +""" +create json for talents for home page +""" +def create_talent_json_from_contribution_json(injson): + out_json_list = [] + + # add talents + for contribution in injson: + try: + for talent in contribution["talents"]: + # need to add contribution id in capability as well + talent["cont_id"] = contribution["id"] + out_json_list.append(talent) + except: + logging.warning("There is no talent in the contribution") + + return out_json_list diff --git a/contributions/catalog/utils/requestutil.py b/contributions/catalog/utils/requestutil.py index a6af4414..006b7101 100644 --- a/contributions/catalog/utils/requestutil.py +++ b/contributions/catalog/utils/requestutil.py @@ -13,8 +13,21 @@ # limitations under the License. import requests +import json + from controllers.config import Config as cfg +"""" +create a request header for endpoint using apikey +""" +def get_header_using_api_key(): + header = { + 'Content-Type': 'application/json', + 'rokwire-api-key': cfg.ROKWIRE_API_KEY + } + + return header + """" create a request header for endpoint using session """ @@ -35,6 +48,17 @@ def get_header_using_auth_token(auth_token): 'Authorization': 'Bearer ' + auth_token } +""" +request contribution list +""" +""" +request reviewer +""" +def request_contributions(headers): + result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL, headers=headers) + + return result + """" reuqest capability using capability id """ @@ -60,3 +84,28 @@ def request_reviewers(headers): result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL +'/admin/reviewers', headers=headers) return result + +""" +request capability list +""" +def request_required_capability_list(headers): + result = requests.get(cfg.CONTRIBUTION_BUILDING_BLOCK_URL + "/capabilities", headers=headers) + + # create the list of required capabilities + if result.status_code == 200: + # check if the login id is in reviewer list + result_str = result.content.decode('utf-8').replace('\n', '') + capability_json_list = json.loads(result_str) + required_capability_list = [] + + # create the list with only the items for required capability + for capability_json in capability_json_list: + contribution_id = capability_json["contributionId"] + capability_name = capability_json["name"] + capability_id = capability_json["id"] + tmp_required_capability = {"contributionId": contribution_id, + "capabilityName": capability_name, + "capabilityId": capability_id} + required_capability_list.append(tmp_required_capability) + + return required_capability_list \ No newline at end of file diff --git a/contributions/catalog/webapps/templates/contribute/capability_details.html b/contributions/catalog/webapps/templates/contribute/capability_details.html index c6627f08..a34dcbeb 100644 --- a/contributions/catalog/webapps/templates/contribute/capability_details.html +++ b/contributions/catalog/webapps/templates/contribute/capability_details.html @@ -11,30 +11,15 @@ - Talent Details + Capability Details - +
- - - + {% include 'topnav.html' %}
- -

Capability Details

@@ -178,6 +163,9 @@

Deployment Details

+ +{% include 'topnav_javascript.html' %} + diff --git a/contributions/catalog/webapps/templates/contribute/contribute.html b/contributions/catalog/webapps/templates/contribute/contribute.html index 372f34b0..fd5940a2 100644 --- a/contributions/catalog/webapps/templates/contribute/contribute.html +++ b/contributions/catalog/webapps/templates/contribute/contribute.html @@ -1,5 +1,5 @@ - + Capability Catalog @@ -54,70 +54,94 @@ + +
- -
- - +
+
+ {% for member in teammembers %} + {{ member['name'] }} + {% endfor %} +
- +
+

Contribution Details

- -
-

Contribution Details

+ {% if is_editable %} + + {% endif %} -
- -
- -

+
+ +
+ {% if is_editable %} + + {% else %} + + {% endif %} +

+
+
+
+ +
+ {% if is_editable %} + + {% else %} + + {% endif %} +
+
+
+ +
+ {% if is_editable %} + + {% else %} + + {% endif %} +
-
-
- -
-
-
-
- -
-
-
- -
-
-
+
+ +
+ {% if is_editable %} +
+ {% else %} +
+ {% endif %} +
@@ -132,83 +156,464 @@

Contribution Details

- + diff --git a/contributions/catalog/webapps/templates/contribute/home.html.old b/contributions/catalog/webapps/templates/contribute/home.html.old new file mode 100644 index 00000000..195fc445 --- /dev/null +++ b/contributions/catalog/webapps/templates/contribute/home.html.old @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + +
+
+ + +
+
+ +
+ + + + + +
+ + +
+ + + + + diff --git a/contributions/catalog/webapps/templates/contribute/submitted.html b/contributions/catalog/webapps/templates/contribute/submitted.html index 76472d12..e610b5e8 100644 --- a/contributions/catalog/webapps/templates/contribute/submitted.html +++ b/contributions/catalog/webapps/templates/contribute/submitted.html @@ -8,8 +8,8 @@ - + - +{% include 'topnav_javascript.html' %}
@@ -145,6 +142,8 @@

Contribution Catalog / Packager

+{% include 'topnav_javascript.html' %} + + diff --git a/contributions/catalog/webapps/templates/topnav.html b/contributions/catalog/webapps/templates/topnav.html new file mode 100644 index 00000000..1b90cbfa --- /dev/null +++ b/contributions/catalog/webapps/templates/topnav.html @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/contributions/catalog/webapps/templates/topnav_javascript.html b/contributions/catalog/webapps/templates/topnav_javascript.html new file mode 100644 index 00000000..38737e44 --- /dev/null +++ b/contributions/catalog/webapps/templates/topnav_javascript.html @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/contributions/contribution.yaml b/contributions/contribution.yaml index 1a8ab1ed..5f5324c3 100644 --- a/contributions/contribution.yaml +++ b/contributions/contribution.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Rokwire Contributions Building Block API description: Contributions Building Block API Documentation - version: 1.11.0 + version: 1.12.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -338,7 +338,13 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Talent' + allOf: + - $ref: '#/components/schemas/Talent' + - type: object + properties: + contributionAdmins: + description: List of GitHub usernames of Contribution admins + type: array 400: description: Bad request 401: @@ -378,7 +384,13 @@ paths: schema: type: array items: - $ref: '#/components/schemas/Capability' + allOf: + - $ref: '#/components/schemas/Capability' + - type: object + properties: + contributionAdmins: + description: List of GitHub usernames of Contribution admins + type: array 400: description: Bad request 401: @@ -480,6 +492,7 @@ components: - name - shortDescription - contributionAdmins + - contacts properties: id: type: string @@ -499,6 +512,25 @@ components: anyOf: - $ref: '#/components/schemas/Person' - $ref: '#/components/schemas/Organization' + contacts: + type: array + items: + properties: + name: + nullable: false + type: string + email: + nullable: false + type: string + phone: + nullable: false + type: string + organization: + nullable: true + type: string + officialAddress: + nullable: true + type: string capabilities: nullable: true type: array @@ -593,6 +625,9 @@ components: type: string version: type: string + versionUrl: + nullable: true + type: string healthCheckUrl: nullable: true type: string @@ -605,26 +640,6 @@ components: description: Deletion endpoint details. E.g. `DELETE /users/details/{UUID}` apiKey: type: string - contacts: - nullable: true - type: array - items: - properties: - name: - nullable: true - type: string - email: - nullable: true - type: string - phone: - nullable: true - type: string - organization: - nullable: true - type: string - officialAddress: - nullable: true - type: string Talent: type: object description: A core feature in the app user interface. @@ -646,7 +661,7 @@ components: requiredCapabilities: type: array items: - $ref: '#/components/schemas/Capability' + $ref: '#/components/schemas/RequiredCapability' requiredBuildingBlocks: type: array items: @@ -706,6 +721,27 @@ components: discloseImageRights: type: string description: Self disclosure about the rights to use images within the Talent + RequiredCapability: + type: object + description: A required capabiity object that recorded in talent + required: + - contributionId + - capabilityId + - capabilityName + properties: + contributionId: + type: string + description: ObjectID formation string identifier of the contribution + contributionName: + type: string + description: Name of the contribution + capabilityId: + type: string + format: uuid + description: UUID4 format string identifier of the capability + capabilityName: + type: string + description: Name of the capability Person: type: object properties: diff --git a/contributions/requirements.txt b/contributions/requirements.txt index bcc4e04a..978cc99d 100644 --- a/contributions/requirements.txt +++ b/contributions/requirements.txt @@ -7,6 +7,6 @@ cryptography==3.3.2 python-dotenv==0.10.3 gunicorn==19.9.0 gevent==20.9.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 ../lib/auth-middleware diff --git a/eventservice/Dockerfile b/eventservice/Dockerfile index dd5eba36..2c25d3cf 100644 --- a/eventservice/Dockerfile +++ b/eventservice/Dockerfile @@ -7,6 +7,7 @@ RUN mkdir -p /app/eventservice/events-images/ WORKDIR /app COPY eventservice/events.yaml /app/ COPY eventservice /app/eventservice/ +COPY eventservice/events.yaml /app/eventservice/api COPY lib /lib/ diff --git a/eventservice/api/controllers/configs.py b/eventservice/api/controllers/configs.py index 375c365c..886109a0 100644 --- a/eventservice/api/controllers/configs.py +++ b/eventservice/api/controllers/configs.py @@ -27,7 +27,10 @@ # set default as empty, since connexion will set events as the root path defined by the yml file. URL_PREFIX = os.getenv("URL_PREFIX", "") -GROUPS_BUILDING_BLOCK_ENDPOINT = os.getenv("GROUPS_BUILDING_BLOCK_ENDPOINT", "https://api-dev.rokwire.illinois.edu/gr/api/int/user/") +GROUPS_BUILDING_BLOCK_HOST= os.getenv("GROUPS_BUILDING_BLOCK_HOST", "https://api-dev.rokwire.illinois.edu/gr") +ROKWIRE_AUTH_HOST = os.getenv("ROKWIRE_AUTH_HOST", "https://api-dev.rokwire.illinois.edu/core") +SHIB_HOST = os.getenv("SHIB_HOST", "shibboleth.illinois.edu") +ROKWIRE_ISSUER = os.getenv('ROKWIRE_ISSUER', 'https://api.rokwire.illinois.edu/') ROKWIRE_GROUPS_API_KEY = os.getenv("ROKWIRE_GROUPS_API_KEY", "") diff --git a/eventservice/api/controllers/events.py b/eventservice/api/controllers/events.py index bc086950..9028b8ac 100644 --- a/eventservice/api/controllers/events.py +++ b/eventservice/api/controllers/events.py @@ -19,6 +19,7 @@ import flask.json import auth_middleware import pymongo +import yaml import utils.jsonutils as jsonutils import utils.mongoutils as mongoutils @@ -219,13 +220,10 @@ def get(event_id): if not result_found: abort(404) json_result = json.loads(result) - if include_private_events: - # check the group id - if result and json_result.get('createdByGroupId') not in group_ids: - abort(401) - else: - # check public group - if result and json_result.get('isGroupPrivate') is True: + + if result and json_result.get('isGroupPrivate') is True: + # private group event + if json_result.get('createdByGroupId') not in group_ids: abort(401) __logger.info("[Get Event]: event id %s", event_id) @@ -659,3 +657,13 @@ def success_response(status_code, msg, event_id): resp.status_code = status_code return make_response(resp) + +def version_search(): + """ + Method to return Events BB version number + Reads yaml file and returns version number + Return : plain text - version number + """ + yaml_file = open('events.yaml') + parsed_appconfig_yaml = yaml.load(yaml_file, Loader=yaml.FullLoader) + return parsed_appconfig_yaml['info']['version'] \ No newline at end of file diff --git a/eventservice/api/utils/group_auth.py b/eventservice/api/utils/group_auth.py index 4f2cdc09..ca0c4f81 100644 --- a/eventservice/api/utils/group_auth.py +++ b/eventservice/api/utils/group_auth.py @@ -1,19 +1,33 @@ import controllers.configs as cfg import requests +import logging from flask import g +logger = logging.getLogger(__name__) + + +def generate_groups_request(): + # Rokwire issuer token + if g.user_token_data.get('iss') == cfg.ROKWIRE_ISSUER: + uin = g.user_token_data.get('uiucedu_uin') + url = "%s%s/groups" % (cfg.GROUPS_BUILDING_BLOCK_HOST + "/api/int/user/", uin) + headers = {"Content-Type": "application/json", + "ROKWIRE_GS_API_KEY": cfg.ROKWIRE_GROUPS_API_KEY} + # Core BB Access Token, Shibboleth ID Token, etc. + else: + id_token = g.user_token + url = cfg.GROUPS_BUILDING_BLOCK_HOST + "/api/user/group-memberships" + headers = {"Content-Type": "application/json", + "Authorization": "Bearer " + id_token} + return url, headers + + def get_group_ids(): group_ids = list() include_private_events = False - # user UserAuth request - if 'user_token_data' in g: + if 'user_token' in g and not g.user_token_data.get('anonymous'): include_private_events = True - auth_resp = g.user_token_data - uin = auth_resp.get('uiucedu_uin') - url = "%s%s/groups" % (cfg.GROUPS_BUILDING_BLOCK_ENDPOINT, uin) - headers = {"Content-Type": "application/json", - "ROKWIRE_GS_API_KEY": cfg.ROKWIRE_GROUPS_API_KEY} - + url, headers = generate_groups_request() req = requests.get(url, headers=headers) if req.status_code == 200: req_data = req.json() @@ -23,18 +37,13 @@ def get_group_ids(): raise Exception("failed to authorize with the groups building block %d" % req.status_code) return include_private_events, group_ids + def get_group_memberships(): group_memberships = list() include_private_events = False - # user UserAuth request - if 'user_token_data' in g: + if 'user_token' in g and not g.user_token_data.get('anonymous'): include_private_events = True - auth_resp = g.user_token_data - uin = auth_resp.get('uiucedu_uin') - url = "%s%s/groups" % (cfg.GROUPS_BUILDING_BLOCK_ENDPOINT, uin) - headers = {"Content-Type": "application/json", - "ROKWIRE_GS_API_KEY": cfg.ROKWIRE_GROUPS_API_KEY} - + url, headers = generate_groups_request() req = requests.get(url, headers=headers) if req.status_code == 200: req_data = req.json() diff --git a/eventservice/events.yaml b/eventservice/events.yaml index 55232703..123b0f56 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.11.0 + version: 1.12.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -14,6 +14,23 @@ tags: - name: Events description: API endpoints for managing events. paths: + /events/version: + get: + tags: + - Events + summary: version endpoint + description: Public endpoint which returns the latest version of events building block + responses: + 200: + description: version returned as text + content: + text/plain: + schema: + type: string + 400: + description: Bad request. + 500: + description: Internal error /events: get: tags: @@ -25,7 +42,7 @@ paths: Auth: Requires a valid API Key or ID Token for access. security: - ApiKeyAuth: [] - - UserAuth: [] + - UserAndCoreTokenAuth: [] parameters: - name: title in: query @@ -217,7 +234,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] requestBody: description: Created event object content: @@ -249,7 +266,7 @@ paths: Auth: Requires a valid API Key or ID Token for access. security: - ApiKeyAuth: [] - - UserAuth: [] + - UserAndCoreTokenAuth: [] parameters: - name: event_id in: path @@ -291,7 +308,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: event_id in: path @@ -342,7 +359,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: event_id in: path @@ -377,7 +394,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: event_id in: path @@ -423,6 +440,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] responses: 200: description: search results matching criteria @@ -448,6 +466,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] responses: 200: description: Getting event tags successful @@ -468,6 +487,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] responses: 200: description: Getting super event tags successful @@ -488,7 +508,7 @@ paths: Auth: Requires a valid API Key or ID Token for access. security: - ApiKeyAuth: [] - - UserAuth: [] + - UserAndCoreTokenAuth: [] parameters: - name: event_id in: path @@ -522,7 +542,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] requestBody: description: Image file to upload and associate with the requested event content: @@ -568,7 +588,7 @@ paths: Auth: Requires a valid API Key or ID Token for access. security: - ApiKeyAuth: [] - - UserAuth: [] + - UserAndCoreTokenAuth: [] parameters: - name: event_id in: path @@ -607,7 +627,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] requestBody: description: Image file to add to the event content: @@ -665,7 +685,7 @@ paths: urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access ``` security: - - UserAuth: [] + - CoreUserAuth: [] parameters: - name: event_id in: path @@ -905,9 +925,21 @@ components: name: ROKWIRE-API-KEY x-apikeyInfoFunc: auth_middleware.verify_apikey description: Each client version has unique API key (e.g., "c6befa22-50a6-4403-a8fc-378c9719743b"). For API endpoints that do not require user authentication, the ROKWIRE-API-KEY header must contain an API key corresponding to a supported client. - UserAuth: + CoreUserAuth: type: http scheme: bearer bearerFormat: JWT # https://openid.net/specs/openid-connect-core-1_0.html [id_token] - x-bearerInfoFunc: auth_middleware.verify_userauth + x-bearerInfoFunc: auth_middleware.verify_core_userauth description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header when making requests to API endpoints that require user authentication. + CoreTokenAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: auth_middleware.verify_core_token + description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header including anonymous tokens + UserAndCoreTokenAuth: + type: http + 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 diff --git a/eventservice/requirements.txt b/eventservice/requirements.txt index 153b909b..e7cb3d8f 100644 --- a/eventservice/requirements.txt +++ b/eventservice/requirements.txt @@ -5,6 +5,6 @@ boto3==1.9.188 python-dotenv==0.10.3 gevent==20.9.0 diskcache==4.0.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 ../lib/auth-middleware diff --git a/lib/auth-middleware/auth_middleware/__init__.py b/lib/auth-middleware/auth_middleware/__init__.py index fe70f59d..f8b662d3 100644 --- a/lib/auth-middleware/auth_middleware/__init__.py +++ b/lib/auth-middleware/auth_middleware/__init__.py @@ -32,17 +32,35 @@ # The header in the request rokwire_api_key_header = 'rokwire-api-key' # Group names for the event and app config manager. These typically come in the is_member_of claim in the id token -rokwire_event_manager_group = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire events manager' -rokwire_events_uploader = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire ems events uploader' -rokwire_events_web_app = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire events web app' -rokwire_events_approvers = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire event approvers' -rokwire_group_admins = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access' -rokwire_app_config_manager_group = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire app config manager' +shib_rokwire_event_manager_group = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire events manager' +shib_rokwire_events_uploader = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire ems events uploader' +shib_rokwire_events_web_app = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire events web app' +shib_rokwire_events_approvers = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire event approvers' +shib_rokwire_group_admins = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire groups access' +shib_rokwire_app_config_manager_group = 'urn:mace:uiuc.edu:urbana:authman:app-rokwire-service-policy-rokwire app config manager' + +rokwire_event_manager_group = 'events_manager' +rokwire_events_uploader = 'events_uploader' +rokwire_events_web_app = 'events_web_app' +rokwire_events_approvers = 'event_approvers' +rokwire_group_admins = 'groups_access' +rokwire_app_config_manager_group = 'app_config_manager' ROKWIRE_EVENT_WRITE_GROUPS = [rokwire_event_manager_group, rokwire_events_uploader, rokwire_events_web_app, rokwire_events_approvers, rokwire_group_admins] ROKWIRE_APP_CONFIG_WRITE_GROUPS = [rokwire_app_config_manager_group] + +ROKWIRE_GROUPS_MAP = { + rokwire_event_manager_group: shib_rokwire_event_manager_group, + rokwire_events_uploader: shib_rokwire_events_uploader, + rokwire_events_web_app: shib_rokwire_events_web_app, + rokwire_events_approvers: shib_rokwire_events_approvers, + rokwire_group_admins: shib_rokwire_group_admins, + rokwire_app_config_manager_group: shib_rokwire_app_config_manager_group +} + # This is the is member of claim name from the +is_member_of_claim = "permissions" uiucedu_is_member_of = "uiucedu_is_member_of" DEBUG_ON = False @@ -79,17 +97,8 @@ def authenticate(group_name=None, internal_token_only=False): should_use_security_token_auth = getattr(view_func, '_use_security_token_auth', False) # print("should use security token auth = %s" % should_use_security_token_auth) - auth_header = request.headers.get('Authorization') - if not auth_header: - logger.warning("Request missing Authorization header") - raise OAuthProblem('Missing authorization header') - ah_split = auth_header.split() - if len(ah_split) != 2 or ah_split[0].lower() != 'bearer': - logger.warning("invalid auth header. expecting 'bearer' and token with space between") - raise OAuthProblem('Invalid request header') - _id_token = ah_split[1] + _id_token = get_bearer_token(request) id_info = verify_userauth(_id_token, group_name, internal_token_only) - return id_info @@ -102,15 +111,28 @@ def authorize(group_name=None): id_info = g.user_token_data if group_name is not None: + ROKWIRE_AUTH_HOST = os.getenv('ROKWIRE_AUTH_HOST', '') + + # So we are to check is a group membership is required. + # Get the membership check keys based on issuer + if id_info['iss'] == ROKWIRE_AUTH_HOST: + is_member_of_key = is_member_of_claim + else: + is_member_of_key = uiucedu_is_member_of + # check if the group_name is list or string if isinstance(group_name, str): # make group name as list group_name = [group_name] is_authorize = False - for name in group_name: - if uiucedu_is_member_of in id_info: - is_member_of = id_info[uiucedu_is_member_of] + if is_member_of_key in id_info: + is_member_of = id_info[is_member_of_key] + if is_member_of_key == is_member_of_claim: + is_member_of = is_member_of.split(',') + for name in group_name: + if id_info['iss'] != ROKWIRE_AUTH_HOST: + name = ROKWIRE_GROUPS_MAP[name] if name in is_member_of: is_authorize = True break @@ -122,6 +144,8 @@ def authorize(group_name=None): # Checks that the request has the right secret for this. This call is used initially and assumes that # the header contains the x-api-key. This (trivially) returns true of the verification worked and # otherwise will return various other exit codes. + + def verify_secret(request): key = request.headers.get(rokwire_api_key_header) if not key: @@ -134,13 +158,44 @@ def verify_secret(request): for test_key in keys: if key == test_key.strip(): # just in case there are embedded blanks return True - raise OAuthProblem('Invalid API Key') # failed matching means unauthorized in this context. + # failed matching means unauthorized in this context. + raise OAuthProblem('Invalid API Key') + + +def verify_core_token(group_name=None): + id_token = get_bearer_token(request) + if not id_token: + raise OAuthProblem('Missing id token') + + # check if its the rokwire core issuer + unverified_header, unverified_payload = get_unverified_header_payload( + id_token) + ROKWIRE_AUTH_HOST = os.getenv('ROKWIRE_AUTH_HOST', '') + ROKWIRE_AUTH_KEY_PATH = os.getenv('ROKWIRE_AUTH_KEY_PATH', '') + issuer = unverified_payload.get('iss') + kid = unverified_header.get('kid') + if not kid: + logger.warning("kid not found. Aborting.") + raise OAuthProblem('Invalid token') + + if issuer == ROKWIRE_AUTH_HOST: + keyset = get_keyset(ROKWIRE_AUTH_HOST + ROKWIRE_AUTH_KEY_PATH) + target_client_ids = re.split( + ',', (os.getenv('ROKWIRE_API_CLIENT_ID', '')).replace(" ", "")) + id_info = decode_id_token(id_token, keyset, target_client_ids, kid) + g.user_token_data = id_info + g.user_token = id_token + return {'id_token_valid': True} + else: + raise OAuthProblem('invalid core token') + def verify_apikey(key, required_scopes=None): if not key: logger.warning("API key is missing the " + rokwire_api_key_header + " header") raise OAuthProblem('Missing API Key') + # Assumption is that the key is a comma separated list of uuid's # This simply turns it in to a list and iterates. If the supplied key is in this list, true is returned # Otherwise, an error is raised. @@ -152,19 +207,105 @@ def verify_apikey(key, required_scopes=None): raise OAuthProblem('Invalid API Key') +def verify_core_userauth(id_token, group_name=None, internal_token_only=False): + id_info = None + if not id_token: + logger.warning("Request missing id token") + raise OAuthProblem('Missing id token') + unverified_header, unverified_payload = get_unverified_header_payload( + id_token) + + if unverified_header.get('phone', False): + # phone number verify -- reject if this should be another type of token. + if internal_token_only: + logger.warning('incorrect id token type.') + raise OAuthProblem('Invalid token') + phone_verify_secret = os.getenv('PHONE_VERIFY_SECRET') + if not phone_verify_secret: + logger.warning("PHONE_VERIFY_SECRET environment variable not set") + raise OAuthProblem('Invalid token') + phone_verify_audience = os.getenv('PHONE_VERIFY_AUDIENCE') + if not phone_verify_audience: + logger.warning("PHONE_VERIFY_AUDIENCE environnment variable not set") + raise OAuthProblem('Invalid token') + try: + id_info = jwt.decode( + id_token, + phone_verify_secret, + audience=phone_verify_audience, + verify=True + ) + except jwt.DecodeError as de: + logger.warning("error on id_token decode. Message = %s" % str(de)) + raise OAuthProblem('Invalid token') + # import pprint; pprint.pprint(id_info) + else: + issuer = unverified_payload.get('iss') + if not issuer: + logger.warning("Issuer not found. Aborting.") + raise OAuthProblem('Invalid token') + kid = unverified_header.get('kid') + if not kid: + logger.warning("kid not found. Aborting.") + raise OAuthProblem('Invalid token') + valid_issuer = False + keyset = None + target_client_ids = None + + SHIB_HOST = os.getenv('SHIBBOLETH_HOST', '') + ROKWIRE_AUTH_HOST = os.getenv('ROKWIRE_AUTH_HOST', '') + ROKWIRE_AUTH_KEY_PATH = os.getenv('ROKWIRE_AUTH_KEY_PATH', '') + ROKWIRE_ISSUER = os.getenv('ROKWIRE_ISSUER', '') + + if issuer == ROKWIRE_AUTH_HOST: + isAnonymous = unverified_payload.get('anonymous') + if isAnonymous is None or isAnonymous: + logger.warning( + "anonymous flag must be set to False") + raise OAuthProblem('Invalid token') + valid_issuer = True + keyset = get_keyset(ROKWIRE_AUTH_HOST + ROKWIRE_AUTH_KEY_PATH) + target_client_ids = re.split( + ',', (os.getenv('ROKWIRE_API_CLIENT_ID', '')).replace(" ", "")) + + elif issuer == ROKWIRE_ISSUER: + valid_issuer = True + lines = base64.b64decode(os.getenv('ROKWIRE_PUB_KEY')) + keyset = json.loads(lines) + target_client_ids = re.split(',', (os.getenv('ROKWIRE_API_CLIENT_ID')).replace(" ", "")) + + elif issuer == 'https://' + SHIB_HOST: + if internal_token_only: + logger.warning("incorrect token type") + raise OAuthProblem('Invalid token') + valid_issuer = True + keyset = get_keyset( + 'https://' + SHIB_HOST + '/idp/profile/oidc/keyset') + target_client_ids = re.split( + ',', (os.getenv('SHIBBOLETH_CLIENT_ID', '')).replace(" ", "")) + + # Comment about the next bit. The Py JWT package's support for getting the keys + # and verifying against said key is (like the rest of it) undocumented. + # These calls may therefore change without warning without notification in future + # releases of that library. If this stops working, check the Py JWT libraries first. + if not valid_issuer: + logger.warning("invalid issuer = %s" % issuer) + raise OAuthProblem('Invalid token') + + id_info = decode_id_token(id_token, keyset, target_client_ids, kid) + # Store ID info for future references in the current request context. + g.user_token_data = id_info + g.user_token = id_token + return id_info + def verify_userauth(id_token, group_name=None, internal_token_only=False): id_info = None if not id_token: logger.warning("Request missing id token") raise OAuthProblem('Missing id token') - try: - # We need to get both the header and the payload initially as unverified since we have to - # check their issuer, key id and a few other items before we can figure out how to unpack them - unverified_header = jwt.get_unverified_header(id_token) - unverified_payload = jwt.decode(id_token, verify=False) - except jwt.exceptions.PyJWTError as jwte: - logger.warning("jwt error on get unverified header. message = %s" % jwte) - raise OAuthProblem('Invalid token') + unverified_header, unverified_payload = get_unverified_header_payload( + id_token) + if unverified_header.get('phone', False): # phone number verify -- reject if this should be another type of token. if internal_token_only: @@ -190,11 +331,8 @@ def verify_userauth(id_token, group_name=None, internal_token_only=False): raise OAuthProblem('Invalid token') # import pprint; pprint.pprint(id_info) else: - # Note there are two cases here that are closely related. Basically we can only differentiate them - # by which issuer is in the id token. - # shibboleth - SHIB_HOST = os.getenv('SHIBBOLETH_HOST') ROKWIRE_ISSUER = os.getenv('ROKWIRE_ISSUER') + SHIB_HOST = os.getenv('SHIBBOLETH_HOST', '') issuer = unverified_payload.get('iss') if not issuer: @@ -220,22 +358,17 @@ def verify_userauth(id_token, group_name=None, internal_token_only=False): # file1.close() lines = base64.b64decode(os.getenv('ROKWIRE_PUB_KEY')) keyset = json.loads(lines) - target_client_ids = re.split(',', (os.getenv('ROKWIRE_API_CLIENT_ID')).replace(" ", "")) - - if issuer == 'https://' + SHIB_HOST: + elif issuer == 'https://' + SHIB_HOST: if internal_token_only: logger.warning("incorrect token type") raise OAuthProblem('Invalid token') valid_issuer = True - keyset_resp = requests.get('https://' + SHIB_HOST + '/idp/profile/oidc/keyset') - if keyset_resp.status_code != 200: - logger.warning("bad status getting keyset. status code = %s" % keyset_resp.status_code) - raise OAuthProblem('Invalid token') - keyset = keyset_resp.json() - - target_client_ids = re.split(',', (os.getenv('SHIBBOLETH_CLIENT_ID')).replace(" ", "")) + keyset = get_keyset( + 'https://' + SHIB_HOST + '/idp/profile/oidc/keyset') + target_client_ids = re.split( + ',', (os.getenv('SHIBBOLETH_CLIENT_ID', '')).replace(" ", "")) # Comment about the next bit. The Py JWT package's support for getting the keys # and verifying against said key is (like the rest of it) undocumented. @@ -245,31 +378,75 @@ def verify_userauth(id_token, group_name=None, internal_token_only=False): logger.warning("invalid issuer = %s" % issuer) raise OAuthProblem('Invalid token') - matching_jwks = [key_dict for key_dict in keyset['keys'] if key_dict['kid'] == kid] - if len(matching_jwks) != 1: - logger.warning("should have exactly one match for kid = %s" % kid) - raise OAuthProblem('Invalid token') - jwk = matching_jwks[0] - pub_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) - try: - id_info = jwt.decode(id_token, key=pub_key, audience=target_client_ids, verify=True) - except jwt.exceptions.PyJWTError as jwte: - logger.warning("jwt error on decode. message = %s" % jwte) - raise OAuthProblem('Invalid token') - if not id_info: - logger.warning("id_info was not returned from decode") - raise OAuthProblem('Invalid token') + id_info = decode_id_token(id_token, keyset, target_client_ids, kid) # Store ID info for future references in the current request context. g.user_token_data = id_info - + g.user_token = id_token return id_info +def verify_userauth_coretoken(group_name=None): + id_token = get_bearer_token(request) + if not id_token: + raise OAuthProblem('Missing id token') + + # check which token auth verify to use + unverified_header, unverified_payload = get_unverified_header_payload( + id_token) + + isAnonymous = unverified_payload.get('anonymous') + if isAnonymous == True: + return verify_core_token(group_name) + return verify_core_userauth(id_token, group_name) + + def use_security_token_auth(func): func._use_security_token_auth = True return func +def get_keyset(request_url): + keyset_resp = requests.get(request_url) + if keyset_resp.status_code != 200: + logger.warning( + "bad status getting keyset. status code = %s" % keyset_resp.status_code) + raise OAuthProblem('Invalid token') + keyset = keyset_resp.json() + return keyset + + +def get_unverified_header_payload(id_token): + try: + # We need to get both the header and the payload initially as unverified since we have to + # check their issuer, key id and a few other items before we can figure out how to unpack them + unverified_header = jwt.get_unverified_header(id_token) + unverified_payload = jwt.decode(id_token, verify=False) + except jwt.exceptions.PyJWTError as jwte: + logger.warning( + "jwt error on get unverified header. message = %s" % jwte) + raise OAuthProblem('Invalid token') + return unverified_header, unverified_payload + + +def decode_id_token(id_token, keyset, target_client_ids, kid): + matching_jwks = [key_dict for key_dict in keyset['keys'] + if key_dict['kid'] == kid] + if len(matching_jwks) != 1: + logger.warning("should have exactly one match for kid = %s" % kid) + raise OAuthProblem('Invalid token') + jwk = matching_jwks[0] + pub_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) + try: + id_info = jwt.decode(id_token, key=pub_key, + audience=target_client_ids, verify=True) + except jwt.exceptions.PyJWTError as jwte: + logger.warning("jwt error on decode. message = %s" % jwte) + raise OAuthProblem('Invalid token') + if not id_info: + logger.warning("id_info was not returned from decode") + raise OAuthProblem('Invalid token') + return id_info + # def authenticate_google(): # from google.oauth2 import id_token # from google.auth.transport import requests as google_auth_requests diff --git a/lib/auth-middleware/readme.md b/lib/auth-middleware/readme.md index 3e88659a..9bd4e05e 100644 --- a/lib/auth-middleware/readme.md +++ b/lib/auth-middleware/readme.md @@ -9,6 +9,15 @@ They should match the ones that are set in the `authservice` flask app. - `SHIBBOLETH_HOST` - `SHIBBOLETH_CLIENT_ID` - `ROKWIRE_API_KEY` +- `ROKWIRE_AUTH_HOST` +- `ROKWIRE_AUTH_KEY_PATH` +- `ROKWIRE_API_CLIENT_ID` SHIBBOLETH_CLIENT_ID can contain one or more client IDs that are separated by comma. -For example, `SHIBBOLETH_CLIENT_ID=abc,def,ghi` \ No newline at end of file +For example, `SHIBBOLETH_CLIENT_ID=abc,def,ghi` +ROKWIRE_AUTH_HOST is the issuer of the token generated by the core building block +For example, `ROKWIRE_AUTH_HOST`=https://api-dev.rokwire.illinois.edu/core +ROKWIRE_AUTH_KEY_PATH is the endpoint for fetching the auth public key +For example, `ROKWIRE_AUTH_KEY_PATH`=/tps/auth-keys +ROKWIRE_API_CLIENT_ID is the audience of the rokwire id token +For example, `ROKWIRE_API_CLIENT_ID`=rokwire \ No newline at end of file diff --git a/loggingservice/logging.yaml b/loggingservice/logging.yaml index 6d7977bd..f8182f72 100755 --- a/loggingservice/logging.yaml +++ b/loggingservice/logging.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Rokwire Logging Building Block API description: Logging Building Block API Documentation - version: 1.11.0 + version: 1.12.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -21,10 +21,10 @@ paths: summary: Create log entries description: | Create log entries from the app. - Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] requestBody: description: A JSON array of one or more log entries. The schema of the request body is only provided as a reference and by default all of its properties are optional. required: true @@ -137,9 +137,15 @@ components: name: ROKWIRE-API-KEY x-apikeyInfoFunc: auth_middleware.verify_apikey description: Each client version has unique API key (e.g., "c6befa22-50a6-4403-a8fc-378c9719743b"). For API endpoints that do not require user authentication, the ROKWIRE-API-KEY header must contain an API key corresponding to a supported client. - UserAuth: + CoreUserAuth: type: http scheme: bearer bearerFormat: JWT # https://openid.net/specs/openid-connect-core-1_0.html [id_token] - x-bearerInfoFunc: auth_middleware.verify_userauth + x-bearerInfoFunc: auth_middleware.verify_core_userauth description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header when making requests to API endpoints that require user authentication. + CoreTokenAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: auth_middleware.verify_core_token + 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 diff --git a/loggingservice/requirements.txt b/loggingservice/requirements.txt index 990ac492..e28bc6b6 100644 --- a/loggingservice/requirements.txt +++ b/loggingservice/requirements.txt @@ -3,6 +3,6 @@ pymongo[tls,srv]==3.7.2 gunicorn==20.0.4 python-dotenv==0.10.3 gevent==20.9.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 ../lib/auth-middleware diff --git a/profileservice/README.md b/profileservice/README.md index d53b4103..65ffce1c 100644 --- a/profileservice/README.md +++ b/profileservice/README.md @@ -339,3 +339,62 @@ API will return the message } ] ``` +### SEARCH profile data using uin or phone +Search existing PII dataset and associated Non-PII dataset by using uin or phone. Used by the Core Building Block to for ROKWIRE 3.0 data migration. +``` +curl http://localhost:5000/profiles/core?uin=&phone= +``` +API will return the message +``` +{ + "pii": { + "lastModifiedDate": "2019/07/03T11:53:12", + "netid": "jd123", + "uin": "2340345", + "firstname": "john", + "pid": "90e7b9ee-de9c-4e2e-a32a-0295e92b035b", + "imageUrl": null, + "email": "jd@testmail.com", + "username": "jd325", + "creationDate": "2019/07/03T11:51:43", + "lastname": "doe", + "phone": "+12345678901", + "uuid":[ + "a6856b73-d453-4515-8002-56e8d0522136" + ] + }, + "non_pii": { + "over13": true, + "uuid": "a6856b73-d453-4515-8002-56e8d0522136", + "interests":[ + { + "subcategories":[ + "Football", + "Basketball" + ], + "category": "Athletics" + }, + { + "category": "Entertainment" + } + ], + "positiveInterestTags":[ + "jazz", + "rock" + ], + "negativeInterestTags":[ + "Rock", + "Hip Hop" + ], + "creationDate": "2019/07/03T11:31:44", + "lastModifiedDate": "2019/07/03T11:37:39", + "favorites":{ + "placeIds":[], + "eventIds":[], + "athleticEventIds":[], + "laundryPlaceIds":[], + "diningPlaceIds":[] + } + } +} +``` \ No newline at end of file diff --git a/profileservice/api/controllers/configs.py b/profileservice/api/controllers/configs.py index d75580fc..c43a0e2b 100644 --- a/profileservice/api/controllers/configs.py +++ b/profileservice/api/controllers/configs.py @@ -35,4 +35,6 @@ PII_DB_PII_COLL_NAME = 'PiiDataset' FIELD_OBJECTID = '_id' FIELD_PROFILE_UUID = 'uuid' -FIELD_PID = 'pid' \ No newline at end of file +FIELD_PID = 'pid' + +ROKWIRE_CORE_BB_API_KEY = os.getenv('ROKWIRE_CORE_BB_API_KEY', '') \ No newline at end of file diff --git a/profileservice/api/controllers/profiles.py b/profileservice/api/controllers/profiles.py index 721b5927..6730f890 100644 --- a/profileservice/api/controllers/profiles.py +++ b/profileservice/api/controllers/profiles.py @@ -234,6 +234,72 @@ 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: + msg = { + "reason": "Unauthorized", + "error": "Unauthorized: " + request.url, + } + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", msg) + logging.error("CORE PROFILE GET " + json.dumps(msg_json)) + return rs_handlers.forbidden(msg_json) + + fields = {} + if uin: + fields['uin'] = uin + if phone: + fields['phone'] = phone + if len(fields) == 0: + msg = { + "reason": "Must provide uin or phone", + "error": "Bad Request: " + request.url, + } + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", msg) + logging.error("CORE PROFILE GET " + json.dumps(msg_json)) + return rs_handlers.bad_request(msg_json) + + if fields != None: + data_list = mongoutils.get_pii_result(fields) + if len(data_list) > 1: + msg = { + "reason": "There is more than 1 pii record: " + str(fields), + "error": "There is more than 1 pii record: " + request.url, + } + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", msg) + logging.error("CORE PROFILE GET " + json.dumps(msg_json)) + return rs_handlers.internal_server_error(msg_json) + if len(data_list) == 0: + msg = {"Not Found": str(fields)} + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", msg) + logging.info("CORE PROFILE GET " + json.dumps(msg_json)) + return mongoutils.construct_json_from_query_list({}) + else: + msg = { + "reason": "Invalid search: " + str(fields), + "error": "Bad Request: " + request.url, + } + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", msg) + logging.error("CORE PROFILE GET " + json.dumps(msg_json)) + return rs_handlers.bad_request(msg_json) + + data_list = jsonutils.remove_file_descriptor_from_data_list(data_list) + uuid_list = data_list[0].get('uuid') + return_data = {"pii": jsonutils.remove_null_fields(data_list[0])} + + if uuid_list != None and len(uuid_list) > 0: + non_pii_data = mongoutils.get_non_pii_query_json_from_field(cfg.FIELD_PROFILE_UUID, uuid_list[0]) + non_pii_data = jsonutils.remove_file_descriptor_from_dataset(non_pii_data) + non_pii_data = jsonutils.remove_null_subcategory(non_pii_data) + return_data["non_pii"] = jsonutils.remove_null_fields(non_pii_data) + + out_json = mongoutils.construct_json_from_query_list(return_data) + msg_json = jsonutils.create_log_json("CORE PROFILE", "GET", return_data) + logging.info("CORE PROFILE GET " + json.dumps(msg_json)) + + currenttime = otherutils.get_current_time_utc() + mongoutils.update_pii_core_migrate_date(fields, currenttime) + + return out_json def pii_post(): # msg = {'message': 'POST info for PII:'} diff --git a/profileservice/api/models/pii_data.py b/profileservice/api/models/pii_data.py index 2eed509f..ed5c70d6 100644 --- a/profileservice/api/models/pii_data.py +++ b/profileservice/api/models/pii_data.py @@ -40,6 +40,7 @@ def __init__(self, injson): self.fileDescriptors = None self.creationDate = None self.lastModifiedDate = None + self.coreMigrateDate = None self = datasetutils.update_pii_dataset_from_json(self, injson) @@ -199,4 +200,10 @@ def set_last_modified_date(self, lastModifiedDate): self.lastModifiedDate = lastModifiedDate def get_last_modified_date(self): - return self.lastModifiedDate \ No newline at end of file + return self.lastModifiedDate + + def set_core_migrate_date(self, coreMigrateDate): + self.coreMigrateDate = coreMigrateDate + + def get_core_migrate_date(self): + return self.coreMigrateDate \ No newline at end of file diff --git a/profileservice/api/utils/datasetutils.py b/profileservice/api/utils/datasetutils.py index 002d571d..3e60058d 100644 --- a/profileservice/api/utils/datasetutils.py +++ b/profileservice/api/utils/datasetutils.py @@ -198,6 +198,10 @@ def update_pii_dataset_from_json(dataset, injson): dataset.set_last_modified_date(injson["lastModifiedDate"]) except Exception as e: pass + try: + dataset.set_core_migrate_date(injson["coreMigrateDate"]) + except Exception as e: + pass return dataset diff --git a/profileservice/api/utils/jsonutils.py b/profileservice/api/utils/jsonutils.py index 723d8662..660186c2 100644 --- a/profileservice/api/utils/jsonutils.py +++ b/profileservice/api/utils/jsonutils.py @@ -42,6 +42,15 @@ def remove_null_subcategory(injson): return injson +def remove_null_fields(injson: dict): + null_keys = [] + for k, v in injson.items(): + if v == None: + null_keys.append(k) + for k in null_keys: + del injson[k] + return injson + def create_log_json(ep_name, ep_method, in_json, data_type=None): if data_type is not None: if data_type.lower() == 'pii': diff --git a/profileservice/api/utils/mongoutils.py b/profileservice/api/utils/mongoutils.py index 6f26a3ab..e8840ee8 100644 --- a/profileservice/api/utils/mongoutils.py +++ b/profileservice/api/utils/mongoutils.py @@ -65,17 +65,7 @@ def get_pii_result(query): return None db_data = db_pii.pii_collection.find(query, {'_id': 0}) - data_list = list(db_data) - - if len(data_list) > 0: - data_dump = dumps(data_list) - data_dump = data_dump[:-1] - data_dump = data_dump[1:] - json_load = json.loads(data_dump) - - return json_load - else: - return None + return list(db_data) """ get query output json from field name and query string @@ -347,6 +337,16 @@ def update_pii_dataset_in_mongo_by_field(fld, query_str, datasetobj): return result.acknowledged, dataset +""" +update pii core migrate date +""" +def update_pii_core_migrate_date(query, core_migrate_date): + if not query: + return None + + result = db_pii.pii_collection.update_one(query, {"$set": {"coreMigrateDate": core_migrate_date}}, upsert=False) + return result.acknowledged + """ index non pii collection """ diff --git a/profileservice/profile.yaml b/profileservice/profile.yaml index e59f49c1..8aa5e369 100755 --- a/profileservice/profile.yaml +++ b/profileservice/profile.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: Rokwire Profile Building Block API description: Profile Building Block API Documentation - version: 1.11.0 + version: 1.12.0 servers: - url: https://api.rokwire.illinois.edu description: Production server @@ -27,6 +27,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] requestBody: description: Created profile object content: @@ -58,6 +59,7 @@ paths: Auth: Requires a valid API Key for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: favorites.eventId in: query @@ -93,6 +95,7 @@ paths: Auth: Requires a valid API Key and profile uuid for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: uuid in: path @@ -128,6 +131,7 @@ paths: Auth: Requires a valid API Key and profile uuid for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: uuid in: path @@ -170,6 +174,7 @@ paths: Auth: Requires a valid API Key and profile uuid for access. security: - ApiKeyAuth: [] + - CoreTokenAuth: [] parameters: - name: uuid in: path @@ -190,6 +195,47 @@ paths: description: Profile not found 500: description: Internal error + /profiles/core: + get: + tags: + - PII + summary: Get profile data by query params + description: | + Returns a single PII entry with associated Non PII information. + + Auth: Requires a valid API Key for access. The API Key must match the value of environment variable ROKWIRE_CORE_BB_API_KEY. This API Key is only used by the Core BB. + parameters: + - name: uin + in: query + description: The parameter for searching PII data by uin + required: false + style: form + explode: true + schema: + type: string + - name: phone + in: query + description: The parameter for searching PII data by phone + required: false + style: form + explode: true + schema: + type: string + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileData' + 400: + description: Invalid params supplied + 401: + description: Unauthorized + 404: + description: Profile not found + 500: + description: Internal error /profiles/pii: post: tags: @@ -289,7 +335,7 @@ paths: summary: Get PII by PID description: | Returns a single PII entry. - + Auth: Requires a valid id_token and PID for access. The contents of the id_token (phone number, uin) must match the PII entry contents. security: - UserAuth: [] @@ -611,6 +657,14 @@ components: uuid: type: string format: uuid + ProfileData: + type: object + nullable: true + properties: + pii: + $ref: '#/components/schemas/Pii' + non_pii: + $ref: '#/components/schemas/Non-pii' securitySchemes: ApiKeyAuth: type: apiKey @@ -624,3 +678,9 @@ components: bearerFormat: JWT # https://openid.net/specs/openid-connect-core-1_0.html [id_token] x-bearerInfoFunc: auth_middleware.verify_userauth description: The client must send a valid (i.e., signed, not expired) OpenID Connect id_token in the Authorization header when making requests to API endpoints that require user authentication. + CoreTokenAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-bearerInfoFunc: auth_middleware.verify_core_token + 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 diff --git a/profileservice/requirements.txt b/profileservice/requirements.txt index 204b9e70..58f904fa 100644 --- a/profileservice/requirements.txt +++ b/profileservice/requirements.txt @@ -8,6 +8,6 @@ cryptography==3.3.2 python-dotenv==0.10.3 gunicorn==20.0.4 gevent==20.9.0 -connexion[swagger-ui]==2.4.0 +connexion[swagger-ui]==2.9.0 ../lib/auth-middleware diff --git a/release_app_config.sh b/release_app_config.sh old mode 100644 new mode 100755 diff --git a/release_base_script.sh b/release_base_script.sh index f2cd0825..5a0c3d23 100644 --- a/release_base_script.sh +++ b/release_base_script.sh @@ -2,6 +2,9 @@ set -e +# Authenticate to ECR +aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 779619664536.dkr.ecr.us-east-2.amazonaws.com + PROJECT_NAME="rokwire" GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" diff --git a/release_contributions.sh b/release_contributions.sh old mode 100644 new mode 100755 diff --git a/release_contributions_catalog.sh b/release_contributions_catalog.sh new file mode 100755 index 00000000..98335c6b --- /dev/null +++ b/release_contributions_catalog.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +source release_base_script.sh + +# Contributions catalog version information +# Docker build args +git_tag=$(git describe --tags $(git rev-list --tags --max-count=1) ) +git_sha=$(git rev-parse --short HEAD) + +###### CONTRIBUTIONS CATALOG ###### +docker build --pull -f contributions/catalog/Dockerfile -t ${PROJECT_NAME}/contributions-catalog:${VERSION} --build-arg GIT_TAG=$git_tag --build-arg GIT_SHA=$git_sha . +docker tag ${PROJECT_NAME}/contributions-catalog:${VERSION} 779619664536.dkr.ecr.us-east-2.amazonaws.com/${PROJECT_NAME}/contributions-catalog:${VERSION} +docker push 779619664536.dkr.ecr.us-east-2.amazonaws.com/${PROJECT_NAME}/contributions-catalog:${VERSION} diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..d89f250a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +detect-secrets==1.0.3 +pre-commit>=2.0.0,<3 \ No newline at end of file