diff --git a/.gitignore b/.gitignore index 720b7aa6c..3001274d9 100644 --- a/.gitignore +++ b/.gitignore @@ -111,4 +111,8 @@ web/yarn-error.log* # Cypress files web/cypress/videos web/cypress/screenshots -web/cypress/downloads \ No newline at end of file +web/cypress/downloads + +# marketplace +local-dev/stack/quay-marketplace-api.crt +local-dev/stack/quay-marketplace-api.key diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd3c0092d..15e6364ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,8 +23,11 @@ repos: entry: web/node_modules/.bin/eslint --fix language: system files: ^web/ + exclude: ^web/cypress/test/ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - id: trailing-whitespace + exclude: ^web/cypress/test/quay-db-data.txt - id: end-of-file-fixer + exclude: ^web/cypress/test/quay-db-data.txt diff --git a/app.py b/app.py index efdb0b191..881299ef5 100644 --- a/app.py +++ b/app.py @@ -53,7 +53,7 @@ from util.greenlet_tracing import enable_tracing from util.ipresolver import IPResolver from util.label_validator import LabelValidator from util.log import filter_logs -from util.marketplace import RHMarketplaceAPI, RHUserAPI +from util.marketplace import MarketplaceSubscriptionApi, MarketplaceUserApi from util.metrics.prometheus import PrometheusPlugin from util.names import urn_generator from util.repomirror.api import RepoMirrorAPI @@ -236,12 +236,6 @@ Principal(app, use_sessions=False) tf = app.config["DB_TRANSACTION_FACTORY"] -rh_user_api = None -rh_marketplace_api = None -if features.ENTITLEMENT_RECONCILIATION or features.RH_MARKETPLACE: - rh_user_api = RHUserAPI(app.config) - rh_marketplace_api = RHMarketplaceAPI(app.config) - model_cache = get_model_cache(app.config) avatar = Avatar(app) login_manager = LoginManager(app) @@ -310,6 +304,9 @@ repo_mirror_api = RepoMirrorAPI( tuf_metadata_api = TUFMetadataAPI(app, app.config) +marketplace_users = MarketplaceUserApi(app) +marketplace_subscriptions = MarketplaceSubscriptionApi(app) + # Check for a key in config. If none found, generate a new signing key for Docker V2 manifests. _v2_key_path = os.path.join(OVERRIDE_CONFIG_DIRECTORY, DOCKER_V2_SIGNINGKEY_FILENAME) if os.path.exists(_v2_key_path): diff --git a/data/database.py b/data/database.py index 16fe8d28f..da3adeffc 100644 --- a/data/database.py +++ b/data/database.py @@ -745,6 +745,7 @@ class User(BaseModel): UserOrganizationQuota, QuotaLimits, RedHatSubscriptions, + OrganizationRhSkus, } | appr_classes | v22_classes @@ -1981,13 +1982,29 @@ class ProxyCacheConfig(BaseModel): class RedHatSubscriptions(BaseModel): """ - Represents internal Red Hat subscriptions for customers + Represents store for users' RH account numbers """ - user_id = ForeignKeyField(User, backref="subscription") + user_id = ForeignKeyField(User, backref="account_number") account_number = IntegerField(unique=True) +class OrganizationRhSkus(BaseModel): + """ + Represents sku to org binding for + RH subscriptions + """ + + subscription_id = IntegerField(index=True, unique=True) + user_id = ForeignKeyField(User, backref="org_bound_subscription") + org_id = ForeignKeyField(User, backref="subscription") + + indexes = ( + (("subscription_id", "org_id"), True), + (("subscription_id", "org_id", "user_id"), True), + ) + + # Defines a map from full-length index names to the legacy names used in our code # to meet length restrictions. LEGACY_INDEX_MAP = { diff --git a/data/migrations/versions/b82361fba1cd_create_table_for_org_to_sku_relations.py b/data/migrations/versions/b82361fba1cd_create_table_for_org_to_sku_relations.py new file mode 100644 index 000000000..582c3072e --- /dev/null +++ b/data/migrations/versions/b82361fba1cd_create_table_for_org_to_sku_relations.py @@ -0,0 +1,54 @@ +"""Create table for org to sku relations + +Revision ID: b82361fba1cd +Revises: 46980ea2dde5 +Create Date: 2023-06-07 14:22:09.384808 + +""" + +# revision identifiers, used by Alembic. +revision = "b82361fba1cd" +down_revision = "8a70b8777089" + +from typing import Text + +import sqlalchemy as sa + + +def upgrade(op, tables, tester): + op.create_table( + "organizationrhskus", + sa.Column("id", sa.Integer, nullable=False, autoincrement=True), + sa.Column("subscription_id", sa.Integer, nullable=False), + sa.Column("org_id", sa.Integer, nullable=False), + sa.Column("user_id", sa.Integer, nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_organizationrhskus")), + sa.ForeignKeyConstraint( + ["user_id"], ["user.id"], name=op.f("fk_organizationrhskus_userid") + ), + sa.ForeignKeyConstraint(["org_id"], ["user.id"], name=op.f("fk_organizationrhskus_orgid")), + ) + op.create_index( + "organizationrhskus_subscription_id", "organizationrhskus", ["subscription_id"], unique=True + ) + op.create_index( + "organizationrhskus_subscription_id_org_id", + "organizationrhskus", + ["subscription_id", "org_id"], + unique=True, + ) + op.create_index( + "organizationrhskus_subscription_id_org_id_user_id", + "organizationrhskus", + ["subscription_id", "org_id", "user_id"], + unique=True, + ) + + +def downgrade(op, tables, tester): + op.drop_index("organizationrhskus_subscription_id", table_name="organizationrhskus") + op.drop_index("organizationrhskus_subscription_id_org_id", table_name="organizationrhskus") + op.drop_index( + "organizationrhskus_subscription_id_org_id_user_id", table_name="organizationrhskus" + ) + op.drop_table("organizationrhskus") diff --git a/data/model/__init__.py b/data/model/__init__.py index 4ee52516d..c98b94f23 100644 --- a/data/model/__init__.py +++ b/data/model/__init__.py @@ -137,6 +137,10 @@ class UnsupportedQuotaSize(DataModelException): pass +class OrgSubscriptionBindingAlreadyExists(DataModelException): + pass + + class TooManyLoginAttemptsException(Exception): def __init__(self, message, retry_after): super(TooManyLoginAttemptsException, self).__init__(message) @@ -178,6 +182,7 @@ from data.model import ( notification, oauth, organization, + organization_skus, permission, proxy_cache, release, diff --git a/data/model/organization_skus.py b/data/model/organization_skus.py new file mode 100644 index 000000000..2cca45d00 --- /dev/null +++ b/data/model/organization_skus.py @@ -0,0 +1,56 @@ +import logging + +import peewee + +from data import model +from data.database import OrganizationRhSkus + +logger = logging.getLogger(__name__) + + +def get_org_subscriptions(org_id): + try: + query = OrganizationRhSkus.select().where(OrganizationRhSkus.org_id == org_id) + return query + except OrganizationRhSkus.DoesNotExist: + return None + + +def bind_subscription_to_org(subscription_id, org_id, user_id): + try: + return OrganizationRhSkus.create( + subscription_id=subscription_id, org_id=org_id, user_id=user_id + ) + except model.DataModelException as ex: + logger.error("Problem binding subscription to org %s: %s", org_id, ex) + except peewee.IntegrityError: + raise model.OrgSubscriptionBindingAlreadyExists() + + +def subscription_bound_to_org(subscription_id): + # lookup row in table matching subscription_id, if there is no row return false, otherwise return true + # this function is used to check if a subscription is bound to an org or + try: + binding = OrganizationRhSkus.get(OrganizationRhSkus.subscription_id == subscription_id) + return True, binding.org_id + except OrganizationRhSkus.DoesNotExist: + return False, None + + +def remove_subscription_from_org(org_id, subscription_id): + query = OrganizationRhSkus.delete().where( + OrganizationRhSkus.org_id == org_id, + OrganizationRhSkus.subscription_id == subscription_id, + ) + query.execute() + + +def remove_all_owner_subscriptions_from_org(user_id, org_id): + try: + query = OrganizationRhSkus.delete().where( + OrganizationRhSkus.user_id == user_id, + OrganizationRhSkus.org_id == org_id, + ) + query.execute() + except model.DataModelException as ex: + raise model.DataModelException(ex) diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index bcf9c4347..2312235d4 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -9,12 +9,13 @@ import stripe from flask import request import features -from app import app, billing, rh_marketplace_api, rh_user_api +from app import app, billing, marketplace_subscriptions, marketplace_users from auth import scopes from auth.auth_context import get_authenticated_user from auth.permissions import AdministerOrganizationPermission from data import model -from data.billing import PLANS, get_plan +from data.billing import PLANS, get_plan, get_plan_using_rh_sku +from data.model import InvalidOrganizationException, organization_skus from endpoints.api import ( ApiResource, abort, @@ -47,11 +48,22 @@ def check_internal_api_for_subscription(namespace_user): Returns subscription from RH marketplace. None returned if no subscription is found. """ - user_account_number = rh_user_api.get_account_number(namespace_user) - if user_account_number: - user_subscriptions = rh_marketplace_api.find_stripe_subscription(user_account_number) - return user_subscriptions - return [] + plans = [] + if namespace_user.organization: + query = organization_skus.get_org_subscriptions(namespace_user.id) + org_subscriptions = list(query.dicts()) if query is not None else [] + for subscription in org_subscriptions: + subscription_id = subscription["subscription_id"] + sku = marketplace_subscriptions.get_subscription_sku(subscription_id) + plans.append(get_plan_using_rh_sku(sku)) + pass + else: + user_account_number = marketplace_users.get_account_number(namespace_user) + if user_account_number: + plans = marketplace_subscriptions.get_list_of_subscriptions( + user_account_number, filter_out_org_bindings=True, convert_to_stripe_plans=True + ) + return plans def get_namespace_plan(namespace): @@ -87,9 +99,11 @@ def lookup_allowed_private_repos(namespace): if features.RH_MARKETPLACE: namespace_user = model.user.get_namespace_user(namespace) - marketplace_subscriptions = check_internal_api_for_subscription(namespace_user) - for subscription in marketplace_subscriptions: - repos_allowed += subscription["privateRepos"] + + subscriptions = check_internal_api_for_subscription(namespace_user) + for subscription in subscriptions: + if subscription is not None: + repos_allowed += subscription["privateRepos"] # Find the number of private repositories used by the namespace and compare it to the # plan subscribed. @@ -916,3 +930,127 @@ class OrganizationInvoiceField(ApiResource): return "Okay", 201 abort(403) + + +@resource("/v1/organization//marketplace") +@path_param("orgname", "The name of the organization") +@show_if(features.BILLING) +class OrganizationRhSku(ApiResource): + """ + Resource for managing an organization's RH SKU + """ + + @require_scope(scopes.ORG_ADMIN) + @nickname("listOrgSkus") + def get(self, orgname): + """ + Get sku assigned to org + """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.organization.get_organization(orgname) + query = model.organization_skus.get_org_subscriptions(organization.id) + + if query: + subscriptions = list(query.dicts()) + for subscription in subscriptions: + subscription["sku"] = marketplace_subscriptions.get_subscription_sku( + subscription["subscription_id"] + ) + return subscriptions + else: + return [] + abort(401) + + @require_scope(scopes.ORG_ADMIN) + @nickname("bindSkuToOrg") + def post(self, orgname): + """ + Assigns a sku to an org + """ + permission = AdministerOrganizationPermission(orgname) + request_data = request.get_json() + subscription_id = request_data["subscription_id"] + if permission.can(): + organization = model.organization.get_organization(orgname) + user = get_authenticated_user() + account_number = marketplace_users.get_account_number(user) + subscriptions = marketplace_subscriptions.get_list_of_subscriptions(account_number) + + if subscriptions is None: + abort(401, message="no valid subscriptions present") + + user_subscription_ids = [int(subscription["id"]) for subscription in subscriptions] + if int(subscription_id) in user_subscription_ids: + try: + model.organization_skus.bind_subscription_to_org( + user_id=user.id, subscription_id=subscription_id, org_id=organization.id + ) + return "Okay", 201 + except model.OrgSubscriptionBindingAlreadyExists: + abort(400, message="subscription is already bound to an org") + else: + abort(401, message=f"subscription does not belong to {user.username}") + + abort(401) + + +@resource("/v1/organization//marketplace/") +@path_param("orgname", "The name of the organization") +@path_param("subscription_id", "Marketplace subscription id") +@related_user_resource(UserPlan) +@show_if(features.BILLING) +class OrganizationRhSkuSubscriptionField(ApiResource): + """ + Resource for removing RH skus from an organization + """ + + @require_scope(scopes.ORG_ADMIN) + def delete(self, orgname, subscription_id): + """ + Remove sku from an org + """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + organization = model.organization.get_organization(orgname) + except InvalidOrganizationException: + return ("Organization not valid", 400) + + model.organization_skus.remove_subscription_from_org(organization.id, subscription_id) + return ("Deleted", 204) + abort(401) + + +@resource("/v1/user/marketplace") +@show_if(features.RH_MARKETPLACE) +class UserSkuList(ApiResource): + """ + Resource for listing a user's RH skus + bound to an org + """ + + @require_user_admin() + @nickname("getUserMarketplaceSubscriptions") + def get(self): + """ + List the invoices for the current user. + """ + user = get_authenticated_user() + account_number = marketplace_users.get_account_number(user) + if not account_number: + raise NotFound() + + user_subscriptions = marketplace_subscriptions.get_list_of_subscriptions(account_number) + + for subscription in user_subscriptions: + bound_to_org, organization = organization_skus.subscription_bound_to_org( + subscription["id"] + ) + # fill in information for whether a subscription is bound to an org + if bound_to_org: + subscription["assigned_to_org"] = organization.username + else: + subscription["assigned_to_org"] = None + + return user_subscriptions diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 7567e50a4..bba3b3886 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -10,7 +10,7 @@ from flask import request import features from app import all_queues, app, authentication, avatar from app import billing as stripe -from app import ip_resolver, namespace_gc_queue, usermanager +from app import ip_resolver, marketplace_subscriptions, namespace_gc_queue, usermanager from auth import scopes from auth.auth_context import get_authenticated_user from auth.permissions import ( @@ -21,8 +21,9 @@ from auth.permissions import ( ViewTeamPermission, ) from data import model -from data.billing import get_plan +from data.billing import get_plan, get_plan_using_rh_sku from data.database import ProxyCacheConfig +from data.model import organization_skus from endpoints.api import ( ApiResource, allow_if_superuser, @@ -42,6 +43,7 @@ from endpoints.api import ( from endpoints.api.user import PrivateRepositories, User from endpoints.exception import NotFound, Unauthorized from proxy import Proxy, UpstreamRegistryError +from util.marketplace import MarketplaceSubscriptionApi from util.names import parse_robot_username from util.request import get_request_ip @@ -364,16 +366,27 @@ class OrgPrivateRepositories(ApiResource): organization = model.organization.get_organization(orgname) private_repos = model.user.get_private_repo_count(organization.username) data = {"privateAllowed": False} + repos_allowed = 0 if organization.stripe_id: cus = stripe.Customer.retrieve(organization.stripe_id) if cus.subscription: - repos_allowed = 0 plan = get_plan(cus.subscription.plan.id) if plan: repos_allowed = plan["privateRepos"] - data["privateAllowed"] = private_repos < repos_allowed + if features.RH_MARKETPLACE: + query = organization_skus.get_org_subscriptions(organization.id) + rh_subscriptions = list(query.dicts()) if query is not None else [] + for subscription in rh_subscriptions: + subscription_sku = marketplace_subscriptions.get_subscription_sku( + subscription["subscription_id"] + ) + equivalent_stripe_plan = get_plan_using_rh_sku(subscription_sku) + if equivalent_stripe_plan: + repos_allowed += equivalent_stripe_plan["privateRepos"] + + data["privateAllowed"] = private_repos < repos_allowed if AdministerOrganizationPermission(orgname).can(): data["privateCount"] = private_repos diff --git a/endpoints/api/team.py b/endpoints/api/team.py index 8bdbd55ef..9c643c329 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -482,6 +482,9 @@ class TeamMember(ApiResource): return "", 204 model.team.remove_user_from_team(orgname, teamname, membername, invoking_user) + if features.RH_MARKETPLACE: + org_id = model.organization.get_organization(orgname).id + model.organization_skus.remove_all_owner_subscriptions_from_org(member.id, org_id) log_action("org_remove_team_member", orgname, {"member": membername, "team": teamname}) return "", 204 diff --git a/endpoints/api/test/test_security.py b/endpoints/api/test/test_security.py index 27cfba9c6..374dffbc9 100644 --- a/endpoints/api/test/test_security.py +++ b/endpoints/api/test/test_security.py @@ -1,3 +1,5 @@ +# isort reordering imports breaks these tests, so tell it to skip +# isort: skip_file from typing import List, Optional, Dict, Tuple, Any, Type from mock import patch @@ -6024,6 +6026,38 @@ SECURITY_TESTS: List[ "devtable", 404, ), + ( + OrganizationRhSkuSubscriptionField, + "DELETE", + {"orgname": "buynlarge", "subscription_id": 12345}, + None, + None, + 401, + ), + ( + OrganizationRhSku, + "GET", + {"orgname": "buynlarge"}, + None, + None, + 401, + ), + ( + OrganizationRhSku, + "POST", + {"orgname": "buynlarge"}, + {"subscription_id": 12345}, + None, + 401, + ), + ( + UserSkuList, + "GET", + {"orgname": "buynlarge"}, + None, + None, + 401, + ), ] diff --git a/endpoints/api/user.py b/endpoints/api/user.py index ac319ce74..3eea21260 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -16,10 +16,10 @@ from app import all_queues, app, authentication, avatar from app import billing as stripe from app import ( ip_resolver, + marketplace_subscriptions, + marketplace_users, namespace_gc_queue, oauth_login, - rh_marketplace_api, - rh_user_api, url_scheme_and_hostname, ) from auth import scopes @@ -638,12 +638,12 @@ class PrivateRepositories(ApiResource): repos_allowed = plan["privateRepos"] if features.RH_MARKETPLACE: # subscriptions in marketplace will get added to private repo count - user_account_number = rh_user_api.get_account_number(user) + user_account_number = marketplace_users.get_account_number(user) if user_account_number: - marketplace_subscriptions = rh_marketplace_api.find_stripe_subscription( - user_account_number + subscriptions = marketplace_subscriptions.get_list_of_subscriptions( + user_account_number, filter_out_org_bindings=True, convert_to_stripe_plans=True ) - for user_subscription in marketplace_subscriptions: + for user_subscription in subscriptions: repos_allowed += user_subscription["privateRepos"] return {"privateCount": private_repos, "privateAllowed": (private_repos < repos_allowed)} diff --git a/initdb.py b/initdb.py index d402ef2ed..859299a14 100644 --- a/initdb.py +++ b/initdb.py @@ -1,3 +1,9 @@ +# isort: skip_file +from typing import Dict, Any +import logging +import json +import hashlib +import random import argparse import calendar import hashlib @@ -1339,6 +1345,7 @@ WHITELISTED_EMPTY_MODELS = [ "Image", "ProxyCacheConfig", "RedHatSubscriptions", + "OrganizationRhSkus", "QuotaRegistrySize", ] diff --git a/test/test_api_usage.py b/test/test_api_usage.py index d3713cd0c..33f8092b0 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -37,6 +37,8 @@ from endpoints.api.billing import ( ListPlans, OrganizationCard, OrganizationPlan, + OrganizationRhSku, + OrganizationRhSkuSubscriptionField, UserCard, UserPlan, ) @@ -5065,5 +5067,62 @@ class TestSuperUserManagement(ApiTestCase): self.assertEqual(len(json["messages"]), 1) +class TestOrganizationRhSku(ApiTestCase): + def test_bind_sku_to_org(self): + self.login(ADMIN_ACCESS_USER) + self.postResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + data={"subscription_id": 12345}, + expected_code=201, + ) + json = self.getJsonResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + ) + self.assertEqual(len(json), 1) + + def test_bind_sku_duplicate(self): + user = model.user.get_user(ADMIN_ACCESS_USER) + org = model.organization.get_organization(ORGANIZATION) + model.organization_skus.bind_subscription_to_org(12345, org.id, user.id) + self.login(ADMIN_ACCESS_USER) + self.postResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + data={"subscription_id": 12345}, + expected_code=400, + ) + + def test_bind_sku_unauthorized(self): + # bind a sku that user does not own + self.login(ADMIN_ACCESS_USER) + self.postResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + data={"subscription_id": 11111}, + expected_code=401, + ) + + def test_remove_sku_from_org(self): + self.login(ADMIN_ACCESS_USER) + self.postResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + data={"subscription_id": 12345}, + expected_code=201, + ) + self.deleteResponse( + resource_name=OrganizationRhSkuSubscriptionField, + params=dict(orgname=ORGANIZATION, subscription_id=12345), + expected_code=204, + ) + json = self.getJsonResponse( + resource_name=OrganizationRhSku, + params=dict(orgname=ORGANIZATION), + ) + self.assertEqual(len(json), 0) + + if __name__ == "__main__": unittest.main() diff --git a/test/testconfig.py b/test/testconfig.py index 271e6f49d..0106451a5 100644 --- a/test/testconfig.py +++ b/test/testconfig.py @@ -112,3 +112,5 @@ class TestConfig(DefaultConfig): FEATURE_PROXY_CACHE = True PERMANENTLY_DELETE_TAGS = True RESET_CHILD_MANIFEST_EXPIRATION = True + + FEATURE_RH_MARKETPLACE = True diff --git a/util/marketplace.py b/util/marketplace.py index fb64f5f63..dfb24de73 100644 --- a/util/marketplace.py +++ b/util/marketplace.py @@ -5,8 +5,8 @@ from datetime import datetime import requests -from data.billing import RH_SKUS, get_plan, get_plan_using_rh_sku -from data.model import entitlements +from data.billing import RH_SKUS, get_plan_using_rh_sku +from data.model import entitlements, organization_skus logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ MARKETPLACE_FILE = "/conf/stack/quay-marketplace-api.crt" MARKETPLACE_SECRET = "/conf/stack/quay-marketplace-api.key" -class RHUserAPI: +class RedHatUserApi(object): def __init__(self, app_config): self.cert = (MARKETPLACE_FILE, MARKETPLACE_SECRET) self.user_endpoint = app_config.get("ENTITLEMENT_RECONCILIATION_USER_ENDPOINT") @@ -52,7 +52,6 @@ class RHUserAPI: } request_url = f"{self.user_endpoint}/v2/findUsers" - r = requests.request( method="post", url=request_url, @@ -69,7 +68,7 @@ class RHUserAPI: return account_number -class RHMarketplaceAPI: +class RedHatSubscriptionApi(object): def __init__(self, app_config): self.cert = (MARKETPLACE_FILE, MARKETPLACE_SECRET) self.marketplace_endpoint = app_config.get( @@ -108,6 +107,7 @@ class RHMarketplaceAPI: now_ms = time.time() * 1000 # Is subscription still valid? if now_ms < end_date: + logger.debug("subscription found for %s", str(skuId)) return subscription return None @@ -165,15 +165,170 @@ class RHMarketplaceAPI: ) return r.status_code - def find_stripe_subscription(self, account_number): + def get_subscription_sku(self, subscription_id): """ - Returns the stripe plan/s relating to marketplace subscriptions - for a given account number + Return the sku for a specific subscription """ - stripe_plans = [] + request_url = f"{self.marketplace_endpoint}/subscription/v5/products/subscription_id={subscription_id}" + request_headers = {"Content-Type": "application/json"} + + try: + r = requests.request( + method="get", + url=request_url, + cert=self.cert, + verify=True, + timeout=REQUEST_TIMEOUT, + headers=request_headers, + ) + + info = json.loads(r.content) + + SubscriptionSKU = info[0]["sku"] + return SubscriptionSKU + except requests.exceptions.SSLError: + raise requests.exceptions.SSLError + + def get_list_of_subscriptions( + self, account_number, filter_out_org_bindings=False, convert_to_stripe_plans=False + ): + """ + Returns a list of all active subscriptions a user has + in RH marketplace + """ + subscription_list = [] for sku in RH_SKUS: user_subscription = self.lookup_subscription(account_number, sku) if user_subscription is not None: - stripe_plans.append(get_plan_using_rh_sku(sku)) + bound_to_org = organization_skus.subscription_bound_to_org(user_subscription["id"]) - return stripe_plans + if filter_out_org_bindings and bound_to_org[0]: + continue + + if convert_to_stripe_plans: + subscription_list.append(get_plan_using_rh_sku(sku)) + else: + # add in sku field for convenience + user_subscription["sku"] = sku + subscription_list.append(user_subscription) + return subscription_list + + +TEST_USER = { + "account_number": 12345, + "email": "test_user@test.com", + "username": "test_user", + "password": "password", +} +FREE_USER = { + "account_number": 23456, + "email": "free_user@test.com", + "username": "free_user", + "password": "password", +} + +DEV_ACCOUNT_NUMBER = 76543 + + +class FakeUserApi(object): + """ + Fake class used for tests + """ + + def lookup_customer_id(self, email): + if email == TEST_USER["email"]: + return TEST_USER["account_number"] + if email == FREE_USER["email"]: + return FREE_USER["account_number"] + return None + + def get_account_number(self, user): + if user.username == "devtable": + return DEV_ACCOUNT_NUMBER + return self.lookup_customer_id(user.email) + + +class FakeSubscriptionApi(object): + """ + Fake class used for tests + """ + + def __init__(self): + self.subscription_extended = False + self.subscription_created = False + + def lookup_subscription(self, customer_id, sku_id): + return None + + def create_entitlement(self, customer_id, sku_id): + self.subscription_created = True + + def extend_subscription(self, subscription_id, end_date): + self.subscription_extended = True + + def get_subscription_sku(self, subscription_id): + if id == 12345: + return "FakeSku" + else: + return None + + def get_list_of_subscriptions( + self, account_number, filter_out_org_bindings=False, convert_to_stripe_plans=False + ): + if account_number == DEV_ACCOUNT_NUMBER: + return [ + { + "id": 12345, + "sku": "FakeSku", + "privateRepos": 0, + } + ] + return [] + + +class MarketplaceUserApi(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.state = self.init_app(app) + else: + self.state = None + + def init_app(self, app): + marketplace_enabled = app.config.get("FEATURE_RH_MARKETPLACE", False) + + marketplace_user_api = FakeUserApi() + + if marketplace_enabled and not app.config.get("TESTING"): + marketplace_user_api = RedHatUserApi(app.config) + + app.extensions = getattr(app, "extensions", {}) + app.extensions["marketplace_user_api"] = marketplace_user_api + return marketplace_user_api + + def __getattr__(self, name): + return getattr(self.state, name, None) + + +class MarketplaceSubscriptionApi(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.state = self.init_app(app) + else: + self.state = None + + def init_app(self, app): + marketplace_enabled = app.config.get("FEATURE_RH_MARKETPLACE", False) + + marketplace_subscription_api = FakeSubscriptionApi() + + if marketplace_enabled and not app.config.get("TESTING"): + marketplace_subscription_api = RedHatSubscriptionApi(app.config) + + app.extensions = getattr(app, "extensions", {}) + app.extensions["marketplace_subscription_api"] = marketplace_subscription_api + return marketplace_subscription_api + + def __getattr__(self, name): + return getattr(self.state, name, None) diff --git a/web/cypress/test/quay-db-data.txt b/web/cypress/test/quay-db-data.txt index 615a7af43..7695c28a0 100644 --- a/web/cypress/test/quay-db-data.txt +++ b/web/cypress/test/quay-db-data.txt @@ -23,7 +23,6 @@ ALTER TABLE IF EXISTS ONLY public.userprompt DROP CONSTRAINT IF EXISTS fk_userpr ALTER TABLE IF EXISTS ONLY public.userorganizationquota DROP CONSTRAINT IF EXISTS fk_userorganizationquota_organization; ALTER TABLE IF EXISTS ONLY public.uploadedblob DROP CONSTRAINT IF EXISTS fk_uploadedblob_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.uploadedblob DROP CONSTRAINT IF EXISTS fk_uploadedblob_blob_id_imagestorage; -ALTER TABLE IF EXISTS ONLY public.torrentinfo DROP CONSTRAINT IF EXISTS fk_torrentinfo_storage_id_imagestorage; ALTER TABLE IF EXISTS ONLY public.teamsync DROP CONSTRAINT IF EXISTS fk_teamsync_team_id_team; ALTER TABLE IF EXISTS ONLY public.teamsync DROP CONSTRAINT IF EXISTS fk_teamsync_service_id_loginservice; ALTER TABLE IF EXISTS ONLY public.teammemberinvite DROP CONSTRAINT IF EXISTS fk_teammemberinvite_user_id_user; @@ -33,20 +32,6 @@ ALTER TABLE IF EXISTS ONLY public.teammember DROP CONSTRAINT IF EXISTS fk_teamme ALTER TABLE IF EXISTS ONLY public.teammember DROP CONSTRAINT IF EXISTS fk_teammember_team_id_team; ALTER TABLE IF EXISTS ONLY public.team DROP CONSTRAINT IF EXISTS fk_team_role_id_teamrole; ALTER TABLE IF EXISTS ONLY public.team DROP CONSTRAINT IF EXISTS fk_team_organization_id_user; -ALTER TABLE IF EXISTS ONLY public.tagtorepositorytag DROP CONSTRAINT IF EXISTS fk_tagtorepositorytag_tag_id_tag; -ALTER TABLE IF EXISTS ONLY public.tagtorepositorytag DROP CONSTRAINT IF EXISTS fk_tagtorepositorytag_repository_tag_id_repositorytag; -ALTER TABLE IF EXISTS ONLY public.tagtorepositorytag DROP CONSTRAINT IF EXISTS fk_tagtorepositorytag_repository_id_repository; -ALTER TABLE IF EXISTS ONLY public.tagmanifesttomanifest DROP CONSTRAINT IF EXISTS fk_tagmanifesttomanifest_tag_manifest_id_tagmanifest; -ALTER TABLE IF EXISTS ONLY public.tagmanifesttomanifest DROP CONSTRAINT IF EXISTS fk_tagmanifesttomanifest_manifest_id_manifest; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabelmap DROP CONSTRAINT IF EXISTS fk_tagmanifestlabelmap_tag_manifest_label_id_tagmanifestlabel; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabelmap DROP CONSTRAINT IF EXISTS fk_tagmanifestlabelmap_tag_manifest_id_tagmanifest; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabelmap DROP CONSTRAINT IF EXISTS fk_tagmanifestlabelmap_manifest_label_id_manifestlabel; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabelmap DROP CONSTRAINT IF EXISTS fk_tagmanifestlabelmap_manifest_id_manifest; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabelmap DROP CONSTRAINT IF EXISTS fk_tagmanifestlabelmap_label_id_label; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabel DROP CONSTRAINT IF EXISTS fk_tagmanifestlabel_repository_id_repository; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabel DROP CONSTRAINT IF EXISTS fk_tagmanifestlabel_label_id_label; -ALTER TABLE IF EXISTS ONLY public.tagmanifestlabel DROP CONSTRAINT IF EXISTS fk_tagmanifestlabel_annotated_id_tagmanifest; -ALTER TABLE IF EXISTS ONLY public.tagmanifest DROP CONSTRAINT IF EXISTS fk_tagmanifest_tag_id_repositorytag; ALTER TABLE IF EXISTS ONLY public.tag DROP CONSTRAINT IF EXISTS fk_tag_tag_kind_id_tagkind; ALTER TABLE IF EXISTS ONLY public.tag DROP CONSTRAINT IF EXISTS fk_tag_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.tag DROP CONSTRAINT IF EXISTS fk_tag_manifest_id_manifest; @@ -56,8 +41,6 @@ ALTER TABLE IF EXISTS ONLY public.star DROP CONSTRAINT IF EXISTS fk_star_reposit ALTER TABLE IF EXISTS ONLY public.servicekey DROP CONSTRAINT IF EXISTS fk_servicekey_approval_id_servicekeyapproval; ALTER TABLE IF EXISTS ONLY public.robotaccounttoken DROP CONSTRAINT IF EXISTS fk_robotaccounttoken_robot_account_id_user; ALTER TABLE IF EXISTS ONLY public.robotaccountmetadata DROP CONSTRAINT IF EXISTS fk_robotaccountmetadata_robot_account_id_user; -ALTER TABLE IF EXISTS ONLY public.repositorytag DROP CONSTRAINT IF EXISTS fk_repositorytag_repository_id_repository; -ALTER TABLE IF EXISTS ONLY public.repositorytag DROP CONSTRAINT IF EXISTS fk_repositorytag_image_id_image; ALTER TABLE IF EXISTS ONLY public.repositorysize DROP CONSTRAINT IF EXISTS fk_repositorysize_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.repositorysearchscore DROP CONSTRAINT IF EXISTS fk_repositorysearchscore_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.repositorypermission DROP CONSTRAINT IF EXISTS fk_repositorypermission_user_id_user; @@ -101,6 +84,8 @@ ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS fk_permissionprototype_delegate_user_id_user; ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS fk_permissionprototype_delegate_team_id_team; ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS fk_permissionprototype_activating_user_id_user; +ALTER TABLE IF EXISTS ONLY public.organizationrhskus DROP CONSTRAINT IF EXISTS fk_organizationrhskus_userid; +ALTER TABLE IF EXISTS ONLY public.organizationrhskus DROP CONSTRAINT IF EXISTS fk_organizationrhskus_orgid; ALTER TABLE IF EXISTS ONLY public.oauthauthorizationcode DROP CONSTRAINT IF EXISTS fk_oauthauthorizationcode_application_id_oauthapplication; ALTER TABLE IF EXISTS ONLY public.oauthapplication DROP CONSTRAINT IF EXISTS fk_oauthapplication_organization_id_user; ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS fk_oauthaccesstoken_authorized_user_id_user; @@ -111,9 +96,6 @@ ALTER TABLE IF EXISTS ONLY public.namespacegeorestriction DROP CONSTRAINT IF EXI ALTER TABLE IF EXISTS ONLY public.messages DROP CONSTRAINT IF EXISTS fk_messages_media_type_id_mediatype; ALTER TABLE IF EXISTS ONLY public.manifestsecuritystatus DROP CONSTRAINT IF EXISTS fk_manifestsecuritystatus_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.manifestsecuritystatus DROP CONSTRAINT IF EXISTS fk_manifestsecuritystatus_manifest_id_manifest; -ALTER TABLE IF EXISTS ONLY public.manifestlegacyimage DROP CONSTRAINT IF EXISTS fk_manifestlegacyimage_repository_id_repository; -ALTER TABLE IF EXISTS ONLY public.manifestlegacyimage DROP CONSTRAINT IF EXISTS fk_manifestlegacyimage_manifest_id_manifest; -ALTER TABLE IF EXISTS ONLY public.manifestlegacyimage DROP CONSTRAINT IF EXISTS fk_manifestlegacyimage_image_id_image; ALTER TABLE IF EXISTS ONLY public.manifestlabel DROP CONSTRAINT IF EXISTS fk_manifestlabel_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.manifestlabel DROP CONSTRAINT IF EXISTS fk_manifestlabel_manifest_id_manifest; ALTER TABLE IF EXISTS ONLY public.manifestlabel DROP CONSTRAINT IF EXISTS fk_manifestlabel_label_id_label; @@ -133,14 +115,9 @@ ALTER TABLE IF EXISTS ONLY public.imagestoragesignature DROP CONSTRAINT IF EXIST ALTER TABLE IF EXISTS ONLY public.imagestoragesignature DROP CONSTRAINT IF EXISTS fk_imagestoragesignature_kind_id_imagestoragesignaturekind; ALTER TABLE IF EXISTS ONLY public.imagestorageplacement DROP CONSTRAINT IF EXISTS fk_imagestorageplacement_storage_id_imagestorage; ALTER TABLE IF EXISTS ONLY public.imagestorageplacement DROP CONSTRAINT IF EXISTS fk_imagestorageplacement_location_id_imagestoragelocation; -ALTER TABLE IF EXISTS ONLY public.image DROP CONSTRAINT IF EXISTS fk_image_storage_id_imagestorage; -ALTER TABLE IF EXISTS ONLY public.image DROP CONSTRAINT IF EXISTS fk_image_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.federatedlogin DROP CONSTRAINT IF EXISTS fk_federatedlogin_user_id_user; ALTER TABLE IF EXISTS ONLY public.federatedlogin DROP CONSTRAINT IF EXISTS fk_federatedlogin_service_id_loginservice; ALTER TABLE IF EXISTS ONLY public.emailconfirmation DROP CONSTRAINT IF EXISTS fk_emailconfirmation_user_id_user; -ALTER TABLE IF EXISTS ONLY public.derivedstorageforimage DROP CONSTRAINT IF EXISTS fk_derivedstorageforimage_transformation_constraint; -ALTER TABLE IF EXISTS ONLY public.derivedstorageforimage DROP CONSTRAINT IF EXISTS fk_derivedstorageforimage_source_image_id_image; -ALTER TABLE IF EXISTS ONLY public.derivedstorageforimage DROP CONSTRAINT IF EXISTS fk_derivedstorageforimage_derivative_id_imagestorage; ALTER TABLE IF EXISTS ONLY public.deletedrepository DROP CONSTRAINT IF EXISTS fk_deletedrepository_repository_id_repository; ALTER TABLE IF EXISTS ONLY public.deletednamespace DROP CONSTRAINT IF EXISTS fk_deletednamespace_namespace_id_user; ALTER TABLE IF EXISTS ONLY public.blobupload DROP CONSTRAINT IF EXISTS fk_blobupload_repository_id_repository; @@ -325,6 +302,9 @@ DROP INDEX IF EXISTS public.permissionprototype_org_id; DROP INDEX IF EXISTS public.permissionprototype_delegate_user_id; DROP INDEX IF EXISTS public.permissionprototype_delegate_team_id; DROP INDEX IF EXISTS public.permissionprototype_activating_user_id; +DROP INDEX IF EXISTS public.organizationrhskus_subscription_id_org_id_user_id; +DROP INDEX IF EXISTS public.organizationrhskus_subscription_id_org_id; +DROP INDEX IF EXISTS public.organizationrhskus_subscription_id; DROP INDEX IF EXISTS public.oauthauthorizationcode_code_name; DROP INDEX IF EXISTS public.oauthauthorizationcode_application_id; DROP INDEX IF EXISTS public.oauthapplication_organization_id; @@ -538,6 +518,7 @@ ALTER TABLE IF EXISTS ONLY public.quayrelease DROP CONSTRAINT IF EXISTS pk_quayr ALTER TABLE IF EXISTS ONLY public.quayregion DROP CONSTRAINT IF EXISTS pk_quayregion; ALTER TABLE IF EXISTS ONLY public.proxycacheconfig DROP CONSTRAINT IF EXISTS pk_proxy_cache_config; ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS pk_permissionprototype; +ALTER TABLE IF EXISTS ONLY public.organizationrhskus DROP CONSTRAINT IF EXISTS pk_organizationrhskus; ALTER TABLE IF EXISTS ONLY public.oauthauthorizationcode DROP CONSTRAINT IF EXISTS pk_oauthauthorizationcode; ALTER TABLE IF EXISTS ONLY public.oauthapplication DROP CONSTRAINT IF EXISTS pk_oauthapplication; ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS pk_oauthaccesstoken; @@ -640,6 +621,7 @@ ALTER TABLE IF EXISTS public.quayrelease ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.quayregion ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.proxycacheconfig ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.permissionprototype ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.organizationrhskus ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.oauthauthorizationcode ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.oauthapplication ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.oauthaccesstoken ALTER COLUMN id DROP DEFAULT; @@ -792,6 +774,8 @@ DROP SEQUENCE IF EXISTS public.proxycacheconfig_id_seq; DROP TABLE IF EXISTS public.proxycacheconfig; DROP SEQUENCE IF EXISTS public.permissionprototype_id_seq; DROP TABLE IF EXISTS public.permissionprototype; +DROP SEQUENCE IF EXISTS public.organizationrhskus_id_seq; +DROP TABLE IF EXISTS public.organizationrhskus; DROP SEQUENCE IF EXISTS public.oauthauthorizationcode_id_seq; DROP TABLE IF EXISTS public.oauthauthorizationcode; DROP SEQUENCE IF EXISTS public.oauthapplication_id_seq; @@ -2786,6 +2770,42 @@ ALTER TABLE public.oauthauthorizationcode_id_seq OWNER TO quay; ALTER SEQUENCE public.oauthauthorizationcode_id_seq OWNED BY public.oauthauthorizationcode.id; +-- +-- Name: organizationrhskus; Type: TABLE; Schema: public; Owner: quay +-- + +CREATE TABLE public.organizationrhskus ( + id integer NOT NULL, + subscription_id integer NOT NULL, + org_id integer NOT NULL, + user_id integer NOT NULL +); + + +ALTER TABLE public.organizationrhskus OWNER TO quay; + +-- +-- Name: organizationrhskus_id_seq; Type: SEQUENCE; Schema: public; Owner: quay +-- + +CREATE SEQUENCE public.organizationrhskus_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.organizationrhskus_id_seq OWNER TO quay; + +-- +-- Name: organizationrhskus_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quay +-- + +ALTER SEQUENCE public.organizationrhskus_id_seq OWNED BY public.organizationrhskus.id; + + -- -- Name: permissionprototype; Type: TABLE; Schema: public; Owner: quay -- @@ -5057,6 +5077,13 @@ ALTER TABLE ONLY public.oauthapplication ALTER COLUMN id SET DEFAULT nextval('pu ALTER TABLE ONLY public.oauthauthorizationcode ALTER COLUMN id SET DEFAULT nextval('public.oauthauthorizationcode_id_seq'::regclass); +-- +-- Name: organizationrhskus id; Type: DEFAULT; Schema: public; Owner: quay +-- + +ALTER TABLE ONLY public.organizationrhskus ALTER COLUMN id SET DEFAULT nextval('public.organizationrhskus_id_seq'::regclass); + + -- -- Name: permissionprototype id; Type: DEFAULT; Schema: public; Owner: quay -- @@ -5437,7 +5464,7 @@ COPY public.accesstokenkind (id, name) FROM stdin; -- COPY public.alembic_version (version_num) FROM stdin; -46980ea2dde5 +b82361fba1cd \. @@ -6271,6 +6298,14 @@ COPY public.oauthauthorizationcode (id, application_id, scope, data, code_creden \. +-- +-- Data for Name: organizationrhskus; Type: TABLE DATA; Schema: public; Owner: quay +-- + +COPY public.organizationrhskus (id, subscription_id, org_id, user_id) FROM stdin; +\. + + -- -- Data for Name: permissionprototype; Type: TABLE DATA; Schema: public; Owner: quay -- @@ -6316,8 +6351,8 @@ COPY public.quayservice (id, name) FROM stdin; -- COPY public.queueitem (id, queue_name, body, available_after, available, processing_expires, retries_remaining, state_id) FROM stdin; -1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2023-06-28 18:17:46.058418 t 2023-06-28 21:12:45.983351 5 f4026e00-acc1-4634-b560-7b9dfe4ea2c0 2 namespacegc/3/ {"marker_id": 2, "original_username": "clair"} 2023-06-28 18:17:51.112431 t 2023-06-28 21:12:51.080949 5 1f7f0b49-2e34-4a00-8414-2559e4bbe63f +1 namespacegc/2/ {"marker_id": 1, "original_username": "quay"} 2023-08-25 15:22:54.808849 t 2023-08-25 18:17:54.783824 5 89f5e206-0b48-4227-8e7f-15e9a549fa2e \. @@ -7951,6 +7986,13 @@ SELECT pg_catalog.setval('public.oauthapplication_id_seq', 1, false); SELECT pg_catalog.setval('public.oauthauthorizationcode_id_seq', 1, false); +-- +-- Name: organizationrhskus_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay +-- + +SELECT pg_catalog.setval('public.organizationrhskus_id_seq', 1, false); + + -- -- Name: permissionprototype_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- @@ -8716,6 +8758,14 @@ ALTER TABLE ONLY public.oauthauthorizationcode ADD CONSTRAINT pk_oauthauthorizationcode PRIMARY KEY (id); +-- +-- Name: organizationrhskus pk_organizationrhskus; Type: CONSTRAINT; Schema: public; Owner: quay +-- + +ALTER TABLE ONLY public.organizationrhskus + ADD CONSTRAINT pk_organizationrhskus PRIMARY KEY (id); + + -- -- Name: permissionprototype pk_permissionprototype; Type: CONSTRAINT; Schema: public; Owner: quay -- @@ -10258,6 +10308,27 @@ CREATE INDEX oauthauthorizationcode_application_id ON public.oauthauthorizationc CREATE UNIQUE INDEX oauthauthorizationcode_code_name ON public.oauthauthorizationcode USING btree (code_name); +-- +-- Name: organizationrhskus_subscription_id; Type: INDEX; Schema: public; Owner: quay +-- + +CREATE UNIQUE INDEX organizationrhskus_subscription_id ON public.organizationrhskus USING btree (subscription_id); + + +-- +-- Name: organizationrhskus_subscription_id_org_id; Type: INDEX; Schema: public; Owner: quay +-- + +CREATE UNIQUE INDEX organizationrhskus_subscription_id_org_id ON public.organizationrhskus USING btree (subscription_id, org_id); + + +-- +-- Name: organizationrhskus_subscription_id_org_id_user_id; Type: INDEX; Schema: public; Owner: quay +-- + +CREATE UNIQUE INDEX organizationrhskus_subscription_id_org_id_user_id ON public.organizationrhskus USING btree (subscription_id, org_id, user_id); + + -- -- Name: permissionprototype_activating_user_id; Type: INDEX; Schema: public; Owner: quay -- @@ -11568,30 +11639,6 @@ ALTER TABLE ONLY public.deletedrepository ADD CONSTRAINT fk_deletedrepository_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); --- --- Name: derivedstorageforimage fk_derivedstorageforimage_derivative_id_imagestorage; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.derivedstorageforimage - ADD CONSTRAINT fk_derivedstorageforimage_derivative_id_imagestorage FOREIGN KEY (derivative_id) REFERENCES public.imagestorage(id); - - --- --- Name: derivedstorageforimage fk_derivedstorageforimage_source_image_id_image; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.derivedstorageforimage - ADD CONSTRAINT fk_derivedstorageforimage_source_image_id_image FOREIGN KEY (source_image_id) REFERENCES public.image(id); - - --- --- Name: derivedstorageforimage fk_derivedstorageforimage_transformation_constraint; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.derivedstorageforimage - ADD CONSTRAINT fk_derivedstorageforimage_transformation_constraint FOREIGN KEY (transformation_id) REFERENCES public.imagestoragetransformation(id); - - -- -- Name: emailconfirmation fk_emailconfirmation_user_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -11616,22 +11663,6 @@ ALTER TABLE ONLY public.federatedlogin ADD CONSTRAINT fk_federatedlogin_user_id_user FOREIGN KEY (user_id) REFERENCES public."user"(id); --- --- Name: image fk_image_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.image - ADD CONSTRAINT fk_image_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); - - --- --- Name: image fk_image_storage_id_imagestorage; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.image - ADD CONSTRAINT fk_image_storage_id_imagestorage FOREIGN KEY (storage_id) REFERENCES public.imagestorage(id); - - -- -- Name: imagestorageplacement fk_imagestorageplacement_location_id_imagestoragelocation; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -11784,30 +11815,6 @@ ALTER TABLE ONLY public.manifestlabel ADD CONSTRAINT fk_manifestlabel_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); --- --- Name: manifestlegacyimage fk_manifestlegacyimage_image_id_image; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.manifestlegacyimage - ADD CONSTRAINT fk_manifestlegacyimage_image_id_image FOREIGN KEY (image_id) REFERENCES public.image(id); - - --- --- Name: manifestlegacyimage fk_manifestlegacyimage_manifest_id_manifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.manifestlegacyimage - ADD CONSTRAINT fk_manifestlegacyimage_manifest_id_manifest FOREIGN KEY (manifest_id) REFERENCES public.manifest(id); - - --- --- Name: manifestlegacyimage fk_manifestlegacyimage_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.manifestlegacyimage - ADD CONSTRAINT fk_manifestlegacyimage_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); - - -- -- Name: manifestsecuritystatus fk_manifestsecuritystatus_manifest_id_manifest; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -11888,6 +11895,22 @@ ALTER TABLE ONLY public.oauthauthorizationcode ADD CONSTRAINT fk_oauthauthorizationcode_application_id_oauthapplication FOREIGN KEY (application_id) REFERENCES public.oauthapplication(id); +-- +-- Name: organizationrhskus fk_organizationrhskus_orgid; Type: FK CONSTRAINT; Schema: public; Owner: quay +-- + +ALTER TABLE ONLY public.organizationrhskus + ADD CONSTRAINT fk_organizationrhskus_orgid FOREIGN KEY (org_id) REFERENCES public."user"(id); + + +-- +-- Name: organizationrhskus fk_organizationrhskus_userid; Type: FK CONSTRAINT; Schema: public; Owner: quay +-- + +ALTER TABLE ONLY public.organizationrhskus + ADD CONSTRAINT fk_organizationrhskus_userid FOREIGN KEY (user_id) REFERENCES public."user"(id); + + -- -- Name: permissionprototype fk_permissionprototype_activating_user_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -12232,22 +12255,6 @@ ALTER TABLE ONLY public.repositorysize ADD CONSTRAINT fk_repositorysize_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); --- --- Name: repositorytag fk_repositorytag_image_id_image; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.repositorytag - ADD CONSTRAINT fk_repositorytag_image_id_image FOREIGN KEY (image_id) REFERENCES public.image(id); - - --- --- Name: repositorytag fk_repositorytag_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.repositorytag - ADD CONSTRAINT fk_repositorytag_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); - - -- -- Name: robotaccountmetadata fk_robotaccountmetadata_robot_account_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -12320,118 +12327,6 @@ ALTER TABLE ONLY public.tag ADD CONSTRAINT fk_tag_tag_kind_id_tagkind FOREIGN KEY (tag_kind_id) REFERENCES public.tagkind(id); --- --- Name: tagmanifest fk_tagmanifest_tag_id_repositorytag; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifest - ADD CONSTRAINT fk_tagmanifest_tag_id_repositorytag FOREIGN KEY (tag_id) REFERENCES public.repositorytag(id); - - --- --- Name: tagmanifestlabel fk_tagmanifestlabel_annotated_id_tagmanifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabel - ADD CONSTRAINT fk_tagmanifestlabel_annotated_id_tagmanifest FOREIGN KEY (annotated_id) REFERENCES public.tagmanifest(id); - - --- --- Name: tagmanifestlabel fk_tagmanifestlabel_label_id_label; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabel - ADD CONSTRAINT fk_tagmanifestlabel_label_id_label FOREIGN KEY (label_id) REFERENCES public.label(id); - - --- --- Name: tagmanifestlabel fk_tagmanifestlabel_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabel - ADD CONSTRAINT fk_tagmanifestlabel_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); - - --- --- Name: tagmanifestlabelmap fk_tagmanifestlabelmap_label_id_label; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabelmap - ADD CONSTRAINT fk_tagmanifestlabelmap_label_id_label FOREIGN KEY (label_id) REFERENCES public.label(id); - - --- --- Name: tagmanifestlabelmap fk_tagmanifestlabelmap_manifest_id_manifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabelmap - ADD CONSTRAINT fk_tagmanifestlabelmap_manifest_id_manifest FOREIGN KEY (manifest_id) REFERENCES public.manifest(id); - - --- --- Name: tagmanifestlabelmap fk_tagmanifestlabelmap_manifest_label_id_manifestlabel; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabelmap - ADD CONSTRAINT fk_tagmanifestlabelmap_manifest_label_id_manifestlabel FOREIGN KEY (manifest_label_id) REFERENCES public.manifestlabel(id); - - --- --- Name: tagmanifestlabelmap fk_tagmanifestlabelmap_tag_manifest_id_tagmanifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabelmap - ADD CONSTRAINT fk_tagmanifestlabelmap_tag_manifest_id_tagmanifest FOREIGN KEY (tag_manifest_id) REFERENCES public.tagmanifest(id); - - --- --- Name: tagmanifestlabelmap fk_tagmanifestlabelmap_tag_manifest_label_id_tagmanifestlabel; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifestlabelmap - ADD CONSTRAINT fk_tagmanifestlabelmap_tag_manifest_label_id_tagmanifestlabel FOREIGN KEY (tag_manifest_label_id) REFERENCES public.tagmanifestlabel(id); - - --- --- Name: tagmanifesttomanifest fk_tagmanifesttomanifest_manifest_id_manifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifesttomanifest - ADD CONSTRAINT fk_tagmanifesttomanifest_manifest_id_manifest FOREIGN KEY (manifest_id) REFERENCES public.manifest(id); - - --- --- Name: tagmanifesttomanifest fk_tagmanifesttomanifest_tag_manifest_id_tagmanifest; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagmanifesttomanifest - ADD CONSTRAINT fk_tagmanifesttomanifest_tag_manifest_id_tagmanifest FOREIGN KEY (tag_manifest_id) REFERENCES public.tagmanifest(id); - - --- --- Name: tagtorepositorytag fk_tagtorepositorytag_repository_id_repository; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagtorepositorytag - ADD CONSTRAINT fk_tagtorepositorytag_repository_id_repository FOREIGN KEY (repository_id) REFERENCES public.repository(id); - - --- --- Name: tagtorepositorytag fk_tagtorepositorytag_repository_tag_id_repositorytag; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagtorepositorytag - ADD CONSTRAINT fk_tagtorepositorytag_repository_tag_id_repositorytag FOREIGN KEY (repository_tag_id) REFERENCES public.repositorytag(id); - - --- --- Name: tagtorepositorytag fk_tagtorepositorytag_tag_id_tag; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.tagtorepositorytag - ADD CONSTRAINT fk_tagtorepositorytag_tag_id_tag FOREIGN KEY (tag_id) REFERENCES public.tag(id); - - -- -- Name: team fk_team_organization_id_user; Type: FK CONSTRAINT; Schema: public; Owner: quay -- @@ -12504,14 +12399,6 @@ ALTER TABLE ONLY public.teamsync ADD CONSTRAINT fk_teamsync_team_id_team FOREIGN KEY (team_id) REFERENCES public.team(id); --- --- Name: torrentinfo fk_torrentinfo_storage_id_imagestorage; Type: FK CONSTRAINT; Schema: public; Owner: quay --- - -ALTER TABLE ONLY public.torrentinfo - ADD CONSTRAINT fk_torrentinfo_storage_id_imagestorage FOREIGN KEY (storage_id) REFERENCES public.imagestorage(id); - - -- -- Name: uploadedblob fk_uploadedblob_blob_id_imagestorage; Type: FK CONSTRAINT; Schema: public; Owner: quay -- diff --git a/workers/reconciliationworker.py b/workers/reconciliationworker.py index 220d22a56..10a7e8146 100644 --- a/workers/reconciliationworker.py +++ b/workers/reconciliationworker.py @@ -5,8 +5,7 @@ import time import features from app import app from app import billing as stripe -from app import rh_marketplace_api as internal_marketplace_api -from app import rh_user_api as internal_user_api +from app import marketplace_subscriptions, marketplace_users from data import model from data.billing import RH_SKUS, get_plan from data.model import entitlements @@ -108,7 +107,7 @@ class ReconciliationWorker(Worker): # try to acquire lock if skip_lock_for_testing: self._perform_reconciliation( - user_api=internal_user_api, marketplace_api=internal_marketplace_api + user_api=marketplace_users, marketplace_api=marketplace_subscriptions ) else: try: @@ -117,7 +116,7 @@ class ReconciliationWorker(Worker): lock_ttl=RECONCILIATION_TIMEOUT + LOCK_TIMEOUT_PADDING, ): self._perform_reconciliation( - user_api=internal_user_api, marketplace_api=internal_marketplace_api + user_api=marketplace_users, marketplace_api=marketplace_subscriptions ) except LockNotAcquiredException: logger.debug("Could not acquire global lock for entitlement reconciliation") diff --git a/workers/test/test_reconciliationworker.py b/workers/test/test_reconciliationworker.py index a9588a513..3f2b90363 100644 --- a/workers/test/test_reconciliationworker.py +++ b/workers/test/test_reconciliationworker.py @@ -1,79 +1,34 @@ import random import string -from datetime import datetime from test.fixtures import * -from unittest.mock import MagicMock, patch - -from dateutil.relativedelta import relativedelta +from unittest.mock import patch from data import model +from util.marketplace import FakeSubscriptionApi, FakeUserApi from workers.reconciliationworker import ReconciliationWorker -TEST_USER = { - "account_number": 12345, - "email": "test_user@test.com", - "username": "test_user", - "password": "password", -} -FREE_USER = { - "account_number": 23456, - "email": "free_user@test.com", - "username": "free_user", - "password": "password", -} - - -class FakeUserApi: - def lookup_customer_id(self, email): - if email == TEST_USER["email"]: - return TEST_USER["account_number"] - if email == FREE_USER["email"]: - return FREE_USER["account_number"] - return None - - -class FakeMarketplaceApi: - def __init__(self): - self.subscription_extended = False - self.subscription_created = False - - def lookup_subscription(self, customerId, sku_id): - return None - - def create_entitlement(self, customerId, skuId): - pass - - -internal_user_api = FakeUserApi() -internal_marketplace_api = FakeMarketplaceApi() +user_api = FakeUserApi() +marketplace_api = FakeSubscriptionApi() worker = ReconciliationWorker() def test_create_for_stripe_user(initialized_db): - test_user = model.user.create_user( - TEST_USER["username"], TEST_USER["password"], TEST_USER["email"] - ) + test_user = model.user.create_user("test_user", "password", "test_user@test.com") test_user.stripe_id = "cus_" + "".join(random.choices(string.ascii_lowercase, k=14)) test_user.save() - with patch.object(internal_marketplace_api, "create_entitlement") as mock: - worker._perform_reconciliation( - user_api=internal_user_api, marketplace_api=internal_marketplace_api - ) + with patch.object(marketplace_api, "create_entitlement") as mock: + worker._perform_reconciliation(user_api=user_api, marketplace_api=marketplace_api) mock.assert_called() def test_skip_free_user(initialized_db): - free_user = model.user.create_user( - FREE_USER["username"], FREE_USER["password"], FREE_USER["email"] - ) + free_user = model.user.create_user("free_user", "password", "free_user@test.com") free_user.save() - with patch.object(internal_marketplace_api, "create_entitlement") as mock: - worker._perform_reconciliation( - user_api=internal_user_api, marketplace_api=internal_marketplace_api - ) + with patch.object(marketplace_api, "create_entitlement") as mock: + worker._perform_reconciliation(user_api=user_api, marketplace_api=marketplace_api) mock.assert_not_called()