def _check_realm_access( self, organization_id, realm_id, user_id, encryption_revision, allowed_roles, expected_maintenance=False, ): try: realm = self._realm_component._get_realm(organization_id, realm_id) except RealmNotFoundError: raise VlobNotFoundError(f"Realm `{realm_id}` doesn't exist") if realm.roles.get(user_id) not in allowed_roles: raise VlobAccessError() if expected_maintenance is False: if realm.status.in_maintenance: raise VlobInMaintenanceError( f"Realm `{realm_id}` is currently under maintenance") elif expected_maintenance is True: if not realm.status.in_maintenance: raise VlobNotInMaintenanceError( f"Realm `{realm_id}` not under maintenance") if encryption_revision not in (None, realm.status.encryption_revision): raise VlobEncryptionRevisionError()
async def update( self, organization_id: OrganizationID, id: UUID, wts: str, version: int, blob: bytes, author: DeviceID, notify_beacon: UUID = None, ) -> None: vlobs = self._organizations[organization_id] try: vlob = vlobs[id] if vlob.wts != wts: raise VlobTrustSeedError() except KeyError: raise VlobNotFoundError() if version - 1 == len(vlob.blob_versions): vlob.blob_versions.append((blob, author)) else: raise VlobVersionError() if notify_beacon: await self.beacon_component.update(organization_id, notify_beacon, id, version, author)
def _get_vlob(self, organization_id: OrganizationID, vlob_id: VlobID) -> Vlob: try: return self._vlobs[(organization_id, vlob_id)] except KeyError: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist")
async def list_versions( self, organization_id: OrganizationID, author: DeviceID, vlob_id: UUID ) -> Dict[int, Tuple[pendulum.Pendulum, DeviceID]]: async with self.dbh.pool.acquire() as conn: async with conn.transaction(): realm_id = await _get_realm_id_from_vlob_id(conn, organization_id, vlob_id) await _check_realm_and_read_access(conn, organization_id, author, realm_id, None) query = """ SELECT version, ({}) as author, created_on FROM vlob_atom WHERE organization = ({}) AND vlob_id = $2 ORDER BY version DESC """.format( q_device(_id=Parameter("author")).select("device_id"), q_organization_internal_id(Parameter("$1")), ) rows = await conn.fetch(query, organization_id, vlob_id) assert rows if not rows: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") return {row["version"]: (row["created_on"], row["author"]) for row in rows}
async def _get_realm_id_from_vlob_id(conn, organization_id: OrganizationID, vlob_id: VlobID) -> RealmID: realm_id_uuid = await conn.fetchval(*_q_get_realm_id_from_vlob_id( organization_id=organization_id.str, vlob_id=vlob_id.uuid)) if not realm_id_uuid: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") return RealmID(realm_id_uuid)
def save_batch(self, batch): for vlob_id, version, data in batch: key = (vlob_id, version) if key in self._done: continue try: del self._todo[key] except KeyError: raise VlobNotFoundError() self._done[key] = data return self._total, len(self._done)
async def _check_realm_access(conn, organization_id, realm_id, author, allowed_roles): rep = await conn.fetchrow( *_q_check_realm_access(organization_id=organization_id, realm_id=realm_id, user_id=author.user_id)) if not rep: raise VlobNotFoundError(f"User `{author.user_id}` doesn't exist") if STR_TO_REALM_ROLE.get(rep[0]) not in allowed_roles: raise VlobAccessError()
async def query_list_versions( conn, organization_id: OrganizationID, author: DeviceID, vlob_id: UUID ) -> Dict[int, Tuple[pendulum.DateTime, DeviceID]]: realm_id = await _get_realm_id_from_vlob_id(conn, organization_id, vlob_id) await _check_realm_and_read_access(conn, organization_id, author, realm_id, None) rows = await conn.fetch(*_q_list_versions(organization_id=organization_id, vlob_id=vlob_id)) assert rows if not rows: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") return {row["version"]: (row["created_on"], row["author"]) for row in rows}
async def query_update( conn, organization_id: OrganizationID, author: DeviceID, encryption_revision: int, vlob_id: VlobID, version: int, timestamp: DateTime, blob: bytes, ) -> None: realm_id = await _get_realm_id_from_vlob_id(conn, organization_id, vlob_id) await _check_realm_and_write_access(conn, organization_id, author, realm_id, encryption_revision, timestamp) previous = await conn.fetchrow(*_q_get_vlob_version( organization_id=organization_id.str, vlob_id=vlob_id.uuid)) if not previous: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") elif previous["version"] != version - 1: raise VlobVersionError() elif previous["created_on"] > timestamp: raise VlobRequireGreaterTimestampError(previous["created_on"]) try: vlob_atom_internal_id = await conn.fetchval(*_q_insert_vlob_atom( organization_id=organization_id.str, author=author.str, realm_id=realm_id.uuid, encryption_revision=encryption_revision, vlob_id=vlob_id.uuid, blob=blob, blob_len=len(blob), timestamp=timestamp, version=version, )) except UniqueViolationError: # Should not occur in theory given we are in a transaction raise VlobVersionError() await _set_vlob_updated(conn, vlob_atom_internal_id, organization_id, author, realm_id, vlob_id, timestamp, version)
async def _check_realm( conn, organization_id, realm_id, encryption_revision, expected_maintenance=False ): try: rep = await get_realm_status(conn, organization_id, realm_id) except RealmNotFoundError as exc: raise VlobNotFoundError(*exc.args) from exc if expected_maintenance is False: if rep["maintenance_type"]: raise VlobInMaintenanceError("Data realm is currently under maintenance") elif expected_maintenance is True: if not rep["maintenance_type"]: raise VlobNotInMaintenanceError(f"Realm `{realm_id}` not under maintenance") if encryption_revision is not None and rep["encryption_revision"] != encryption_revision: raise VlobEncryptionRevisionError()
async def _get_realm_id_from_vlob_id(conn, organization_id, vlob_id): query = """ SELECT realm.realm_id FROM vlob_atom INNER JOIN vlob_encryption_revision ON vlob_atom.vlob_encryption_revision = vlob_encryption_revision._id INNER JOIN realm ON vlob_encryption_revision.realm = realm._id WHERE vlob_atom._id = ({}) LIMIT 1 """.format( q_vlob_atom(organization_id=Parameter("$1"), vlob_id=Parameter("$2")).select("_id").limit(1) ) realm_id = await conn.fetchval(query, organization_id, vlob_id) if not realm_id: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") return realm_id
def get_reencrypted_vlobs(self): assert self.is_finished() vlobs = {} for (vlob_id, version), data in sorted(self._done.items()): try: (_, author, timestamp) = self._original_vlobs[vlob_id].data[version - 1] except KeyError: raise VlobNotFoundError() if vlob_id not in vlobs: vlobs[vlob_id] = Vlob(self.realm_id, [(data, author, timestamp)]) else: vlobs[vlob_id].data.append((data, author, timestamp)) assert len(vlobs[vlob_id].data) == version return vlobs
async def read( self, organization_id: OrganizationID, id: UUID, rts: str, version: int = None ) -> Tuple[int, bytes]: vlobs = self._organizations[organization_id] try: vlob = vlobs[id] if vlob.rts != rts: raise VlobTrustSeedError() except KeyError: raise VlobNotFoundError() if version is None: version = len(vlob.blob_versions) try: return (version, vlob.blob_versions[version - 1][0]) except IndexError: raise VlobVersionError()
async def _check_realm_access( conn, organization_id: OrganizationID, realm_id: RealmID, author: DeviceID, allowed_roles: Tuple[RealmRole, ...], ) -> DateTime: rep = await conn.fetchrow( *_q_check_realm_access(organization_id=organization_id.str, realm_id=realm_id.uuid, user_id=author.user_id.str)) if not rep: raise VlobNotFoundError(f"User `{author.user_id}` doesn't exist") role = RealmRole(rep[0]) if rep[0] is not None else None if role not in allowed_roles: raise VlobAccessError() role_granted_on = rep[1] return role_granted_on
async def _check_realm_access(conn, organization_id, realm_id, author, allowed_roles): query = """ WITH cte_current_realm_roles AS ( SELECT DISTINCT ON(user_) user_, role FROM realm_user_role WHERE realm = ({}) ORDER BY user_, certified_on DESC ) SELECT role FROM user_ LEFT JOIN cte_current_realm_roles ON user_._id = cte_current_realm_roles.user_ WHERE user_._id = ({}) """.format( q_realm_internal_id(organization_id=Parameter("$1"), realm_id=Parameter("$2")), q_user_internal_id(organization_id=Parameter("$1"), user_id=Parameter("$3")), ) rep = await conn.fetchrow(query, organization_id, realm_id, author.user_id) if not rep: raise VlobNotFoundError(f"User `{author.user_id}` doesn't exist") if STR_TO_REALM_ROLE.get(rep[0]) not in allowed_roles: raise VlobAccessError()
async def update( self, organization_id: OrganizationID, author: DeviceID, encryption_revision: int, vlob_id: UUID, version: int, timestamp: pendulum.Pendulum, blob: bytes, ) -> None: async with self.dbh.pool.acquire() as conn, conn.transaction(): realm_id = await _get_realm_id_from_vlob_id(conn, organization_id, vlob_id) await _check_realm_and_write_access( conn, organization_id, author, realm_id, encryption_revision ) query = """ SELECT version, created_on FROM vlob_atom WHERE organization = ({}) AND vlob_id = $2 ORDER BY version DESC LIMIT 1 """.format( q_organization_internal_id(Parameter("$1")) ) previous = await conn.fetchrow(query, organization_id, vlob_id) if not previous: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") elif previous["version"] != version - 1: raise VlobVersionError() elif previous["created_on"] > timestamp: raise VlobTimestampError() query = """ INSERT INTO vlob_atom ( organization, vlob_encryption_revision, vlob_id, version, blob, size, author, created_on ) SELECT ({}), ({}), $5, $9, $6, $7, ({}), $8 RETURNING _id """.format( q_organization_internal_id(Parameter("$1")), q_vlob_encryption_revision_internal_id( organization_id=Parameter("$1"), realm_id=Parameter("$3"), encryption_revision=Parameter("$4"), ), q_device_internal_id(organization_id=Parameter("$1"), device_id=Parameter("$2")), ) try: vlob_atom_internal_id = await conn.fetchval( query, organization_id, author, realm_id, encryption_revision, vlob_id, blob, len(blob), timestamp, version, ) except UniqueViolationError: # Should not occurs in theory given we are in a transaction raise VlobVersionError() await _vlob_updated( conn, vlob_atom_internal_id, organization_id, author, realm_id, vlob_id, version )
def _check_realm_access(self, organization_id, realm_id, user_id, encryption_revision, operation_kind): try: realm = self._realm_component._get_realm(organization_id, realm_id) except RealmNotFoundError: raise VlobNotFoundError(f"Realm `{realm_id}` doesn't exist") # Only an owner can perform maintenance operation if operation_kind == OperationKind.MAINTENANCE: allowed_roles = (RealmRole.OWNER, ) # All roles can do read-only operation elif operation_kind == OperationKind.DATA_READ: allowed_roles = ( RealmRole.OWNER, RealmRole.MANAGER, RealmRole.CONTRIBUTOR, RealmRole.READER, ) # All roles except reader can do write operation elif operation_kind == OperationKind.DATA_WRITE: allowed_roles = (RealmRole.OWNER, RealmRole.MANAGER, RealmRole.CONTRIBUTOR) else: assert False, f"Operation kind {operation_kind} not supported" # Check the role if realm.roles.get(user_id) not in allowed_roles: raise VlobAccessError() # Special case of reading while in reencryption if operation_kind == OperationKind.DATA_READ and realm.status.in_reencryption: # Starting a reencryption maintenance bumps the encryption revision. # Hence if we are currently in reencryption maintenance, last encryption revision is not ready # to be used (it will be once the reencryption is over !). # So during this intermediary state, we allow read access to the previous encryption revision instead. # Note that `encryption_revision` might also be `None` in the case of `poll_changes` and `list_versions` # requests, which should also be allowed during a reencryption # The vlob is not available yet for the current revision if (encryption_revision is not None and encryption_revision == realm.status.encryption_revision): raise VlobInMaintenanceError( f"Realm `{realm_id}` is currently under maintenance") # The vlob is only available at the previous revision if (encryption_revision is not None and encryption_revision != realm.status.encryption_revision - 1): raise VlobEncryptionRevisionError() # In all other cases else: # Writing during maintenance is forbidden if operation_kind != OperationKind.MAINTENANCE and realm.status.in_maintenance: raise VlobInMaintenanceError( f"Realm `{realm_id}` is currently under maintenance") # A maintenance state was expected if operation_kind == OperationKind.MAINTENANCE and not realm.status.in_maintenance: raise VlobNotInMaintenanceError( f"Realm `{realm_id}` not under maintenance") # Otherwise, simply check that the revisions match if (encryption_revision is not None and encryption_revision != realm.status.encryption_revision): raise VlobEncryptionRevisionError()
async def update( self, organization_id: OrganizationID, id: UUID, wts: str, version: int, blob: bytes, author: DeviceID, notify_beacon: UUID = None, ) -> None: async with self.dbh.pool.acquire() as conn: async with conn.transaction(): previous = await conn.fetchrow( """ SELECT wts, version, rts FROM vlobs WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND vlob_id = $2 ORDER BY version DESC LIMIT 1 """, organization_id, id, ) if not previous: raise VlobNotFoundError() elif previous[0] != wts: raise VlobTrustSeedError() elif previous[1] != version - 1: raise VlobVersionError() rts = previous[2] try: result = await conn.execute( """ INSERT INTO vlobs ( organization, vlob_id, rts, wts, version, blob, author ) SELECT _id, $2, $3, $4, $5, $6, ( SELECT _id FROM devices WHERE device_id = $7 AND organization = organizations._id ) FROM organizations WHERE organization_id = $1 """, organization_id, id, rts, wts, version, blob, author, ) except UniqueViolationError: # Should not occurs in theory given we are in a transaction raise VlobVersionError() if result != "INSERT 0 1": raise VlobError(f"Insertion error: {result}") if notify_beacon: await self.beacon_component.ll_update( conn, organization_id, notify_beacon, id, version, author)
async def read(self, organization_id: OrganizationID, id: UUID, rts: str, version: int = None) -> Tuple[int, bytes]: async with self.dbh.pool.acquire() as conn: async with conn.transaction(): if version is None: data = await conn.fetchrow( """ SELECT rts, version, blob FROM vlobs WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND vlob_id = $2 ORDER BY version DESC LIMIT 1 """, organization_id, id, ) if not data: raise VlobNotFoundError() else: data = await conn.fetchrow( """ SELECT rts, version, blob FROM vlobs WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND vlob_id = $2 AND version = $3 """, organization_id, id, version, ) if not data: # TODO: not cool to need 2nd request to know the error... exists = await conn.fetchrow( """ SELECT true FROM vlobs WHERE organization = ( SELECT _id from organizations WHERE organization_id = $1 ) AND vlob_id = $2 """, organization_id, id, ) if exists: raise VlobVersionError() else: raise VlobNotFoundError() if data["rts"] != rts: raise VlobTrustSeedError() return data[1:]
async def _get_realm_id_from_vlob_id(conn, organization_id, vlob_id): realm_id = await conn.fetchval(*_q_get_realm_id_from_vlob_id( organization_id=organization_id, vlob_id=vlob_id)) if not realm_id: raise VlobNotFoundError(f"Vlob `{vlob_id}` doesn't exist") return realm_id