diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f87dd0e470..d56ad37ef9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,7 +27,7 @@ jobs: matrix: test_type: ['UNIT', 'INDEX', 'DOCKER'] # We are really not set up for these next two to be multiplicative, so be careful adding more. - python_version: ['3.8', '3.11'] + python_version: ['3.8'] node_version: ['18'] # Steps represent a sequence of tasks that will be executed as part of the job diff --git a/pyproject.toml b/pyproject.toml index 3521153d94..fafbdc9265 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,13 +33,10 @@ classifiers = [ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', ] [tool.poetry.dependencies] -python = ">=3.8.1,<3.12" +python = ">=3.8.1,<3.10" awscli = ">=1.29.54" boto3 = "^1.28.54" botocore = "^1.31.54" @@ -91,7 +88,7 @@ python-dateutil = "^2.8.2" # python-magic is presently pinned to 0.4.15 in lockstep with dcicsnovault's requirements. See explanation there. python_magic = ">=0.4.24,<1" pytz = ">=2021.3" -PyYAML = "^6.0.1" +PyYAML = "5.3.1" rdflib = "^4.2.2" rdflib-jsonld = ">=0.5.0,<1.0.0" # repoze.debug is needed to use pyramid.pserve - Will Feb 17 2022 @@ -181,7 +178,6 @@ dis2pheno = "encoded.commands.parse_hpoa:main" es-index-data = "snovault.commands.es_index_data:main" export-data = "encoded.commands.export_data:main" extract-test-data = "encoded.commands.extract_test_data:main" -generate-local-access-key = "snovault.commands.generate_local_access_key:main" import-data = "encoded.commands.import_data:main" jsonld-rdf = "encoded.commands.jsonld_rdf:main" load-access-keys = "encoded.commands.load_access_keys:main" @@ -206,7 +202,6 @@ submission-test = "encoded.commands.submission_test:main" # submit-metadata-bundle = "encoded.commands.submit_metadata_bundle:main" update-inserts-from-server = "encoded.commands.update_inserts_from_server:main" verify-item = "encoded.commands.verify_item:main" -view-local-object= "snovault.commands.view_local_object:main" # cgap-specific commands clear-variants-and-genes = "encoded.commands.clear_variants_and_genes:main" gene-table-intake = "encoded.commands.gene_table_intake:main" diff --git a/src/encoded/ingestion/table_utils.py b/src/encoded/ingestion/table_utils.py index f5714680f5..3a9c825267 100644 --- a/src/encoded/ingestion/table_utils.py +++ b/src/encoded/ingestion/table_utils.py @@ -3,7 +3,7 @@ import io import json import logging -from collections.abc import Mapping +from collections import Mapping from ..util import resolve_file_path logger = logging.getLogger(__name__) diff --git a/src/encoded/schemas/access_key.json b/src/encoded/schemas/access_key.json new file mode 100644 index 0000000000..af8c34dca7 --- /dev/null +++ b/src/encoded/schemas/access_key.json @@ -0,0 +1,69 @@ +{ + "title": "Admin access key", + "$id": "/profiles/access_key.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [], + "additionalProperties": false, + "mixinProperties": [ + { + "$ref": "mixins.json#/schema_version" + }, + { + "$ref": "mixins.json#/uuid" + }, + { + "$ref": "mixins.json#/submitted" + }, + { + "$ref": "mixins.json#/modified" + } + ], + "type": "object", + "properties": { + "schema_version": { + "default": "1" + }, + "status": { + "title": "Status", + "type": "string", + "default": "current", + "enum": [ + "current", + "deleted" + ] + }, + "user": { + "title": "User", + "comment": "Only admins are allowed to set this value.", + "type": "string", + "linkTo": "User" + }, + "description": { + "title": "Description", + "type": "string", + "formInput": "textarea" + }, + "access_key_id": { + "title": "Access key ID", + "comment": "Only admins are allowed to set this value.", + "type": "string", + "uniqueKey": true + }, + "secret_access_key_hash": { + "title": "Secret access key Hash", + "comment": "Only admins are allowed to set this value.", + "type": "string" + }, + "expiration_date": { + "Title": "Expiration Date", + "comment": "Only admins are allowed to set this value.", + "type": "string", + "permission": "restricted_fields" + } + }, + "facets": { + "user.display_title": { + "title": "User Name" + } + } +} diff --git a/src/encoded/types/access_key.py b/src/encoded/types/access_key.py new file mode 100644 index 0000000000..5fe8e01ee1 --- /dev/null +++ b/src/encoded/types/access_key.py @@ -0,0 +1,161 @@ +"""Access_key types file.""" + +from pyramid.view import view_config +from pyramid.security import ( + Allow, + Deny, + Authenticated, + Everyone, +) +from pyramid.settings import asbool +import datetime +from .base import ( + Item, + DELETED_ACL, + ONLY_ADMIN_VIEW_ACL, +) +from ..authentication import ( + generate_password, + generate_user, + CRYPT_CONTEXT, +) +from snovault import ( + collection, + load_schema, +) +from snovault.crud_views import ( + collection_add, + item_edit, +) +from snovault.validators import ( + validate_item_content_post, +) +from snovault.util import debug_log + +@collection( + name='access-keys', + unique_key='access_key:access_key_id', + properties={ + 'title': 'Access keys', + 'description': 'Programmatic access keys', + }, + acl=[ + (Allow, Authenticated, 'add'), + (Allow, 'group.admin', 'list'), + (Allow, 'group.read-only-admin', 'list'), + (Allow, 'remoteuser.INDEXER', 'list'), + (Allow, 'remoteuser.EMBED', 'list'), + (Deny, Everyone, 'list'), + ]) +class AccessKey(Item): + """AccessKey class.""" + ACCESS_KEY_EXPIRATION_TIME = 90 # days + item_type = 'access_key' + schema = load_schema('encoded:schemas/access_key.json') + name_key = 'access_key_id' + embedded_list = [] + + STATUS_ACL = { + 'current': [(Allow, 'role.owner', ['view', 'edit'])] + ONLY_ADMIN_VIEW_ACL, + 'deleted': DELETED_ACL, + } + + @classmethod + def create(cls, registry, uuid, properties, sheets=None): + """ Sets the access key timeout 90 days from creation. """ + properties['expiration_date'] = (datetime.datetime.utcnow() + datetime.timedelta( + days=cls.ACCESS_KEY_EXPIRATION_TIME)).isoformat() + return super().create(registry, uuid, properties, sheets) + + def __ac_local_roles__(self): + """grab and return user as owner.""" + owner = 'userid.%s' % self.properties['user'] + return {owner: 'role.owner'} + + def __json__(self, request): + """delete the secret access key has from the object when used.""" + properties = super(AccessKey, self).__json__(request) + del properties['secret_access_key_hash'] + return properties + + def update(self, properties, sheets=None): + """smth.""" + # make sure PUTs preserve the secret access key hash + if 'secret_access_key_hash' not in properties: + new_properties = self.properties.copy() + new_properties.update(properties) + properties = new_properties + # set new expiration + properties['expiration_date'] = (datetime.datetime.utcnow() + datetime.timedelta( + days=self.ACCESS_KEY_EXPIRATION_TIME)).isoformat() + self._update(properties, sheets) + + class Collection(Item.Collection): + pass + + +# access keys have view permissions for update so readonly admin and the like +# can create access keys to download files. +@view_config(context=AccessKey.Collection, request_method='POST', + permission='add', + validators=[validate_item_content_post]) +@debug_log +def access_key_add(context, request): + """smth.""" + crypt_context = request.registry[CRYPT_CONTEXT] + + if 'access_key_id' not in request.validated: + request.validated['access_key_id'] = generate_user() + + if 'user' not in request.validated: + request.validated['user'], = [ + principal.split('.', 1)[1] + for principal in request.effective_principals + if principal.startswith('userid.') + ] + + password = None + if 'secret_access_key_hash' not in request.validated: + password = generate_password() + request.validated['secret_access_key_hash'] = crypt_context.hash(password) + + result = collection_add(context, request) + + if password is None: + result['secret_access_key'] = None + else: + result['secret_access_key'] = password + + result['access_key_id'] = request.validated['access_key_id'] + result['description'] = request.validated.get('description', "") + return result + + +@view_config(name='reset-secret', context=AccessKey, + permission='add', + request_method='POST', subpath_segments=0) +@debug_log +def access_key_reset_secret(context, request): + """smth.""" + request.validated = context.properties.copy() + crypt_context = request.registry[CRYPT_CONTEXT] + password = generate_password() + new_hash = crypt_context.hash(password) + request.validated['secret_access_key_hash'] = new_hash + result = item_edit(context, request, render=False) + result['access_key_id'] = request.validated['access_key_id'] + result['secret_access_key'] = password + return result + + +@view_config(context=AccessKey, permission='view_raw', request_method='GET', + name='raw') +@debug_log +def access_key_view_raw(context, request): + """smth.""" + if asbool(request.params.get('upgrade', True)): + properties = context.upgrade_properties() + else: + properties = context.properties.copy() + del properties['secret_access_key_hash'] + return properties