1
0
mirror of https://github.com/quay/quay.git synced 2025-07-30 07:43:13 +03:00

oauth: allowing to assign token to user (PROJQUAY-7074) (#2869)

Allow organization administrators to assign Oauth token to another user.
This commit is contained in:
Brandon Caton
2024-06-25 09:23:51 -04:00
committed by GitHub
parent bc06a3ef36
commit e4f05583c1
24 changed files with 1331 additions and 41 deletions

View File

@ -51,6 +51,7 @@ CLIENT_WHITELIST = [
"UI_V2_FEEDBACK_FORM", "UI_V2_FEEDBACK_FORM",
"TERMS_OF_SERVICE_URL", "TERMS_OF_SERVICE_URL",
"UI_DELAY_AFTER_WRITE_SECONDS", "UI_DELAY_AFTER_WRITE_SECONDS",
"FEATURE_ASSIGN_OAUTH_TOKEN",
] ]
@ -887,3 +888,5 @@ class DefaultConfig(ImmutableConfig):
# whitelist for ROBOTS_DISALLOW to grant access/usage for mirroring # whitelist for ROBOTS_DISALLOW to grant access/usage for mirroring
ROBOTS_WHITELIST: Optional[List[str]] = [] ROBOTS_WHITELIST: Optional[List[str]] = []
FEATURE_ASSIGN_OAUTH_TOKEN = True

View File

@ -751,6 +751,7 @@ class User(BaseModel):
NamespaceAutoPrunePolicy, NamespaceAutoPrunePolicy,
AutoPruneTaskStatus, AutoPruneTaskStatus,
RepositoryAutoPrunePolicy, RepositoryAutoPrunePolicy,
OauthAssignedToken,
} }
| appr_classes | appr_classes
| v22_classes | v22_classes
@ -2035,6 +2036,19 @@ class RepositoryAutoPrunePolicy(BaseModel):
policy = JSONField(null=False, default={}) policy = JSONField(null=False, default={})
class OauthAssignedToken(BaseModel):
uuid = CharField(default=uuid_generator, max_length=36, index=True, unique=True, null=False)
assigned_user = QuayUserField(index=True, unique=False, null=False)
application = ForeignKeyField(OAuthApplication, index=True, null=False, unique=False)
redirect_uri = CharField(max_length=255, null=True)
scope = CharField(max_length=255, null=False)
response_type = CharField(max_length=255, null=True)
class Meta:
database = db
read_only_config = read_only_config
# Defines a map from full-length index names to the legacy names used in our code # Defines a map from full-length index names to the legacy names used in our code
# to meet length restrictions. # to meet length restrictions.
LEGACY_INDEX_MAP = { LEGACY_INDEX_MAP = {

View File

@ -0,0 +1,60 @@
"""add oauth assigned token table
Revision ID: 0988213e0885
Revises: 0cdd1f27a450
Create Date: 2024-05-15 09:09:58.088599
"""
# revision identifiers, used by Alembic.
revision = "0988213e0885"
down_revision = "0cdd1f27a450"
import sqlalchemy as sa
def upgrade(op, tables, tester):
op.create_table(
"oauthassignedtoken",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("uuid", sa.String(length=255), nullable=False),
sa.Column("assigned_user_id", sa.Integer(), nullable=False),
sa.Column("application_id", sa.Integer(), nullable=False),
sa.Column("redirect_uri", sa.String(length=255), nullable=True),
sa.Column("scope", sa.String(length=255), nullable=False),
sa.Column("response_type", sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(
["application_id"],
["oauthapplication.id"],
name=op.f("fk_oauthassignedtoken_application_oauthapplication"),
),
sa.ForeignKeyConstraint(
["assigned_user_id"],
["user.id"],
name=op.f("fk_oauthassignedtoken_assigned_user_user"),
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_oauthassignedtoken")),
)
op.create_index("oauthassignedtoken_uuid", "oauthassignedtoken", ["uuid"], unique=True)
op.create_index(
"oauthassignedtoken_application_id", "oauthassignedtoken", ["application_id"], unique=False
)
op.create_index(
"oauthassignedtoken_assigned_user", "oauthassignedtoken", ["assigned_user_id"], unique=False
)
op.bulk_insert(
tables.notificationkind,
[
{"name": "assigned_authorization"},
],
)
def downgrade(op, tables, tester):
op.drop_table("oauthassignedtoken")
op.execute(
tables.notificationkind.delete().where(
tables.notificationkind.c.name == op.inline_literal("assigned_authorization")
)
)

View File

@ -8,12 +8,14 @@ from auth import scopes
from data.database import ( from data.database import (
OAuthAccessToken, OAuthAccessToken,
OAuthApplication, OAuthApplication,
OauthAssignedToken,
OAuthAuthorizationCode, OAuthAuthorizationCode,
User, User,
random_string_generator, random_string_generator,
) )
from data.fields import Credential, DecryptedValue from data.fields import Credential, DecryptedValue
from data.model import config, user from data.model import config, db_transaction, notification, user
from data.model.organization import is_org_admin
from oauth import utils from oauth import utils
from oauth.provider import AuthorizationProvider from oauth.provider import AuthorizationProvider
from util import get_app_url from util import get_app_url
@ -189,15 +191,20 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
return self._make_redirect_error_response(redirect_uri, "authorization_denied") return self._make_redirect_error_response(redirect_uri, "authorization_denied")
def get_token_response(self, response_type, client_id, redirect_uri, **params): def get_token_response(
self, response_type, client_id, redirect_uri, assignment_uuid=None, **params
):
# Ensure proper response_type # Ensure proper response_type
if response_type != "token": if response_type != "token":
err = "unsupported_response_type" err = "unsupported_response_type"
return self._make_redirect_error_response(redirect_uri, err) return self._make_redirect_error_response(redirect_uri, err)
# Check for a valid client ID. # Check for a valid client ID.
is_valid_client_id = self.validate_client_id(client_id) oauth_application = self.get_application_for_client_id(client_id)
if not is_valid_client_id:
if oauth_application is None or not self.is_org_admin_or_has_token_assignment(
oauth_application.organization, assignment_uuid
):
err = "unauthorized_client" err = "unauthorized_client"
return self._make_redirect_error_response(redirect_uri, err) return self._make_redirect_error_response(redirect_uri, err)
@ -239,6 +246,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
data=data, data=data,
) )
# check for assignment_id and delete it if it exists
if assignment_uuid is not None:
user_obj = self.get_authorized_user()
assign = get_token_assignment_for_client_id(assignment_uuid, user_obj, client_id)
if assign is not None:
assign.delete_instance()
url = utils.build_url(redirect_uri, params) url = utils.build_url(redirect_uri, params)
url += "#access_token=%s&token_type=%s&expires_in=%s" % ( url += "#access_token=%s&token_type=%s&expires_in=%s" % (
access_token, access_token,
@ -274,6 +288,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
def discard_refresh_token(self, client_id, refresh_token): def discard_refresh_token(self, client_id, refresh_token):
raise NotImplementedError() raise NotImplementedError()
def is_org_admin_or_has_token_assignment(self, organization, assignment_uuid):
return (
is_org_admin(self.get_authorized_user(), organization)
or get_token_assignment(assignment_uuid, self.get_authorized_user(), organization)
is not None
)
def create_application(org, name, application_uri, redirect_uri, **kwargs): def create_application(org, name, application_uri, redirect_uri, **kwargs):
client_secret = kwargs.pop("client_secret", random_string_generator(length=40)()) client_secret = kwargs.pop("client_secret", random_string_generator(length=40)())
@ -337,12 +358,15 @@ def lookup_application(org, client_id):
def delete_application(org, client_id): def delete_application(org, client_id):
application = lookup_application(org, client_id) with db_transaction():
if not application: application = lookup_application(org, client_id)
return if not application:
return
application.delete_instance(recursive=True, delete_nullable=True) OauthAssignedToken.delete().where(OauthAssignedToken.application == application).execute()
return application
application.delete_instance(recursive=True, delete_nullable=True)
return application
def lookup_access_token_by_uuid(token_uuid): def lookup_access_token_by_uuid(token_uuid):
@ -378,12 +402,42 @@ def list_access_tokens_for_user(user_obj):
return query return query
def get_assigned_authorization_for_user(user_obj, uuid):
try:
assigned_token = (
OauthAssignedToken.select()
.join(OAuthApplication)
.where(OauthAssignedToken.assigned_user == user_obj, OauthAssignedToken.uuid == uuid)
.get()
)
return assigned_token
except OauthAssignedToken.DoesNotExist:
return None
def list_assigned_authorizations_for_user(user_obj):
query = (
OauthAssignedToken.select()
.join(OAuthApplication)
.where(OauthAssignedToken.assigned_user == user_obj)
)
return query
def list_applications_for_org(org): def list_applications_for_org(org):
query = OAuthApplication.select().join(User).where(OAuthApplication.organization == org) query = OAuthApplication.select().join(User).where(OAuthApplication.organization == org)
return query return query
def get_oauth_application_for_client_id(client_id):
try:
return OAuthApplication.get(client_id=client_id)
except OAuthApplication.DoesNotExist:
return None
def create_user_access_token(user_obj, client_id, scope, access_token=None, expires_in=9000): def create_user_access_token(user_obj, client_id, scope, access_token=None, expires_in=9000):
access_token = access_token or random_string_generator(length=40)() access_token = access_token or random_string_generator(length=40)()
token_name = access_token[:ACCESS_TOKEN_PREFIX_LENGTH] token_name = access_token[:ACCESS_TOKEN_PREFIX_LENGTH]
@ -406,3 +460,59 @@ def create_user_access_token(user_obj, client_id, scope, access_token=None, expi
data="", data="",
) )
return created, access_token return created, access_token
def assign_token_to_user(application, user, redirect_uri, scope, response_type):
with db_transaction():
token = OauthAssignedToken.create(
application=application,
assigned_user=user,
redirect_uri=redirect_uri,
scope=scope,
response_type=response_type,
)
notification.create_notification(
"assigned_authorization",
user,
{
"username": user.username,
},
)
return token
def get_token_assignment(uuid, db_user, org):
if uuid is None:
return None
try:
return (
OauthAssignedToken.select()
.join(OAuthApplication)
.where(
OauthAssignedToken.uuid == uuid,
OauthAssignedToken.assigned_user == db_user,
OAuthApplication.organization == org,
)
.get()
)
except OauthAssignedToken.DoesNotExist:
return None
def get_token_assignment_for_client_id(uuid, user, client_id):
try:
return (
OauthAssignedToken.select()
.join(OAuthApplication)
.where(
OauthAssignedToken.uuid == uuid,
OauthAssignedToken.assigned_user == user,
OAuthApplication.client_id == client_id,
)
.get()
)
except OauthAssignedToken.DoesNotExist:
return None

View File

@ -194,3 +194,14 @@ def add_user_as_admin(user_obj, org_obj):
team.add_user_to_team(user_obj, admin_team) team.add_user_to_team(user_obj, admin_team)
except team.UserAlreadyInTeam: except team.UserAlreadyInTeam:
pass pass
def is_org_admin(user, org):
return (
Team.select()
.join(TeamMember)
.switch(Team)
.join(TeamRole)
.where(Team.organization == org, TeamRole.name == "admin", TeamMember.user == user)
.exists()
)

View File

@ -0,0 +1,96 @@
from unittest.mock import patch
from auth.scopes import READ_REPO
from data import model
from data.model.oauth import DatabaseAuthorizationProvider
from test.fixtures import *
REDIRECT_URI = "http://foo/bar/baz"
class MockDatabaseAuthorizationProvider(DatabaseAuthorizationProvider):
def get_authorized_user(self):
return model.user.get_user("devtable")
def setup(num_assignments=0):
user = model.user.get_user("devtable")
org = model.organization.get_organization("buynlarge")
application = model.oauth.create_application(org, "test", "http://foo/bar", REDIRECT_URI)
token_assignment = model.oauth.assign_token_to_user(
application, user, REDIRECT_URI, READ_REPO.scope, "token"
)
return (application, token_assignment, user, org)
def test_get_token_response_with_assignment_id(initialized_db):
application, token_assignment, user, org = setup()
with patch("data.model.oauth.url_for", return_value="http://foo/bar/baz"):
db_auth_provider = MockDatabaseAuthorizationProvider()
response = db_auth_provider.get_token_response(
"token",
application.client_id,
REDIRECT_URI,
token_assignment.uuid,
scope=READ_REPO.scope,
)
assert response.status_code == 302
assert "error" not in response.headers["Location"]
assert model.oauth.get_token_assignment(token_assignment.uuid, user, org) is None
def test_delete_application(initialized_db):
application, token_assignment, user, org = setup()
model.oauth.delete_application(org, application.client_id)
assert model.oauth.get_token_assignment(token_assignment.uuid, user, org) is None
assert model.oauth.lookup_application(org, application.client_id) is None
def test_get_assigned_authorization_for_user(initialized_db):
application, token_assignment, user, org = setup()
assigned_oauth = model.oauth.get_assigned_authorization_for_user(user, token_assignment.uuid)
assert assigned_oauth is not None
assert assigned_oauth.uuid == token_assignment.uuid
def test_list_assigned_authorizations_for_user(initialized_db):
application, token_assignment, user, org = setup()
second_token_assignment = model.oauth.assign_token_to_user(
application, user, REDIRECT_URI, READ_REPO.scope, "token"
)
assigned_oauths = model.oauth.list_assigned_authorizations_for_user(user)
assert len(assigned_oauths) == 3
assert assigned_oauths[1].uuid == token_assignment.uuid
assert assigned_oauths[2].uuid == second_token_assignment.uuid
def test_get_oauth_application_for_client_id(initialized_db):
application, token_assignment, user, org = setup()
assert model.oauth.get_oauth_application_for_client_id(application.client_id) == application
def test_assign_token_to_user(initialized_db):
application, token_assignment, user, org = setup()
created_assignment = model.oauth.get_token_assignment(token_assignment.uuid, user, org)
assert created_assignment is not None
assert created_assignment.uuid == token_assignment.uuid
assert created_assignment.application == application
assert created_assignment.assigned_user == user
assert created_assignment.redirect_uri == REDIRECT_URI
assert created_assignment.scope == READ_REPO.scope
assert created_assignment.response_type == "token"
def test_get_token_assignment_for_client_id(initialized_db):
application, token_assignment, user, org = setup()
assert (
model.oauth.get_token_assignment_for_client_id(
token_assignment.uuid, user, application.client_id
)
== token_assignment
)

View File

@ -1,10 +1,9 @@
from test.fixtures import *
import pytest import pytest
from data.model.organization import get_organization, get_organizations from data.model.organization import get_organization, get_organizations, is_org_admin
from data.model.user import mark_namespace_for_deletion from data.model.user import get_user, mark_namespace_for_deletion
from data.queue import WorkQueue from data.queue import WorkQueue
from test.fixtures import *
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -25,3 +24,9 @@ def test_get_organizations(deleted, initialized_db):
deleted_found = [org for org in orgs if org.id == deleted_org.id] deleted_found = [org for org in orgs if org.id == deleted_org.id]
assert bool(deleted_found) == deleted assert bool(deleted_found) == deleted
def test_is_org_admin(initialized_db):
user = get_user("devtable")
org = get_organization("sellnsmall")
assert is_org_admin(user, org) is True

View File

@ -1,15 +1,19 @@
from datetime import datetime from datetime import datetime
from test.fixtures import *
from test.helpers import check_transitive_modifications
import pytest import pytest
from mock import patch from mock import patch
from auth.scopes import READ_REPO
from data import model from data import model
from data.database import DeletedNamespace, EmailConfirmation, FederatedLogin, User from data.database import DeletedNamespace, EmailConfirmation, FederatedLogin, User
from data.fields import Credential from data.fields import Credential
from data.model.notification import create_notification from data.model.notification import create_notification
from data.model.organization import get_organization from data.model.oauth import (
assign_token_to_user,
get_oauth_application_for_client_id,
get_token_assignment,
)
from data.model.organization import create_organization, get_organization
from data.model.repository import create_repository from data.model.repository import create_repository
from data.model.team import add_user_to_team, create_team from data.model.team import add_user_to_team, create_team
from data.model.user import ( from data.model.user import (
@ -26,6 +30,7 @@ from data.model.user import (
get_public_repo_count, get_public_repo_count,
get_pull_credentials, get_pull_credentials,
get_quay_user_from_federated_login_name, get_quay_user_from_federated_login_name,
get_user,
list_namespace_robots, list_namespace_robots,
lookup_robot, lookup_robot,
mark_namespace_for_deletion, mark_namespace_for_deletion,
@ -34,6 +39,8 @@ from data.model.user import (
verify_robot, verify_robot,
) )
from data.queue import WorkQueue from data.queue import WorkQueue
from test.fixtures import *
from test.helpers import check_transitive_modifications
from util.security.token import encode_public_private_token from util.security.token import encode_public_private_token
from util.timedeltastring import convert_to_timedelta from util.timedeltastring import convert_to_timedelta
@ -96,7 +103,7 @@ def test_get_active_users(disabled, deleted, initialized_db):
assert user.enabled assert user.enabled
def test_mark_namespace_for_deletion(initialized_db): def test_mark_user_for_deletion(initialized_db):
def create_transaction(db): def create_transaction(db):
return db.transaction() return db.transaction()
@ -117,6 +124,16 @@ def test_mark_namespace_for_deletion(initialized_db):
assert FederatedLogin.select().where(FederatedLogin.user == user).count() == 2 assert FederatedLogin.select().where(FederatedLogin.user == user).count() == 2
assert FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists() assert FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists()
# Add an oauth assigned token
org = model.organization.get_organization("buynlarge")
application = model.oauth.create_application(
org, "test", "http://foo/bar", "http://foo/bar/baz"
)
assigned_token = assign_token_to_user(
application, user, "http://foo/bar/baz", READ_REPO.scope, "token"
)
assert get_token_assignment(assigned_token.uuid, user, org) is not None
# Mark the user for deletion. # Mark the user for deletion.
queue = WorkQueue("testgcnamespace", create_transaction) queue = WorkQueue("testgcnamespace", create_transaction)
mark_namespace_for_deletion(user, [], queue) mark_namespace_for_deletion(user, [], queue)
@ -140,6 +157,9 @@ def test_mark_namespace_for_deletion(initialized_db):
not FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists() not FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists()
) )
# Ensure the oauth assigned token is gone
assert get_token_assignment(assigned_token.uuid, user, org) is None
# Ensure we can create a user with the same namespace again. # Ensure we can create a user with the same namespace again.
new_user = create_user_noverify("foobar", "foo@example.com", email_required=False) new_user = create_user_noverify("foobar", "foo@example.com", email_required=False)
assert new_user.id != user.id assert new_user.id != user.id
@ -148,6 +168,76 @@ def test_mark_namespace_for_deletion(initialized_db):
assert User.get(id=user.id).username != "foobar" assert User.get(id=user.id).username != "foobar"
def test_mark_organization_for_deletion(initialized_db):
def create_transaction(db):
return db.transaction()
# Create a user and then mark it for deletion.
user = get_user("devtable")
org = create_organization("foobar", "foobar@devtable.com", user)
# Add some robots.
create_robot("foo", org)
create_robot("bar", org)
assert lookup_robot("foobar+foo") is not None
assert lookup_robot("foobar+bar") is not None
assert len(list(list_namespace_robots("foobar"))) == 2
# Add some federated user links.
attach_federated_login(org, "google", "someusername")
attach_federated_login(org, "github", "someusername")
assert FederatedLogin.select().where(FederatedLogin.user == org).count() == 2
assert FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists()
# Add an oauth assigned token and application
application1 = model.oauth.create_application(
org, "test", "http://foo/bar", "http://foo/bar/baz"
)
application = model.oauth.create_application(
org, "test2", "http://foo/bar", "http://foo/bar/baz"
)
assigned_token = assign_token_to_user(
application, user, "http://foo/bar/baz", READ_REPO.scope, "token"
)
assert get_token_assignment(assigned_token.uuid, user, org) is not None
# Mark the user for deletion.
queue = WorkQueue("testgcnamespace", create_transaction)
mark_namespace_for_deletion(org, [], queue)
# Ensure the older user is still in the DB.
older_user = User.get(id=org.id)
assert older_user.username != "foobar"
# Ensure the robots are deleted.
with pytest.raises(InvalidRobotException):
assert lookup_robot("foobar+foo")
with pytest.raises(InvalidRobotException):
assert lookup_robot("foobar+bar")
assert len(list(list_namespace_robots(older_user.username))) == 0
# Ensure the federated logins are gone.
assert FederatedLogin.select().where(FederatedLogin.user == org).count() == 0
assert (
not FederatedLogin.select().where(FederatedLogin.service_ident == "someusername").exists()
)
# Ensure the oauth assigned token is gone
assert get_oauth_application_for_client_id(application1.client_id) is None
assert get_oauth_application_for_client_id(application.client_id) is None
assert get_token_assignment(assigned_token.uuid, org, org) is None
# Ensure we can create a user with the same namespace again.
new_org = create_organization("foobar", "foobar@devtable.com", user)
assert new_org.id != org.id
# Ensure the older org is still in the DB.
assert User.get(id=org.id).username != "foobar"
def test_delete_namespace_via_marker(initialized_db): def test_delete_namespace_via_marker(initialized_db):
def create_transaction(db): def create_transaction(db):
return db.transaction() return db.transaction()

View File

@ -19,6 +19,7 @@ from data.database import (
NamespaceAutoPrunePolicy, NamespaceAutoPrunePolicy,
NamespaceGeoRestriction, NamespaceGeoRestriction,
OAuthApplication, OAuthApplication,
OauthAssignedToken,
QuotaNamespaceSize, QuotaNamespaceSize,
RepoMirrorConfig, RepoMirrorConfig,
Repository, Repository,
@ -1355,6 +1356,7 @@ def _delete_user_linked_data(user):
# Delete any OAuth approvals and tokens associated with the user. # Delete any OAuth approvals and tokens associated with the user.
with db_transaction(): with db_transaction():
for app in OAuthApplication.select().where(OAuthApplication.organization == user): for app in OAuthApplication.select().where(OAuthApplication.organization == user):
OauthAssignedToken.delete().where(OauthAssignedToken.application == app).execute()
app.delete_instance(recursive=True) app.delete_instance(recursive=True)
else: else:
# Remove the user from any teams in which they are a member. # Remove the user from any teams in which they are a member.
@ -1394,6 +1396,9 @@ def _delete_user_linked_data(user):
# Delete the quota size entry # Delete the quota size entry
QuotaNamespaceSize.delete().where(QuotaNamespaceSize.namespace_user == user).execute() QuotaNamespaceSize.delete().where(QuotaNamespaceSize.namespace_user == user).execute()
# Delete any oauth assigned tokens
OauthAssignedToken.delete().where(OauthAssignedToken.assigned_user == user).execute()
def get_pull_credentials(robotname): def get_pull_credentials(robotname):
""" """

View File

@ -5075,6 +5075,35 @@ SECURITY_TESTS: List[
(UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "devtable", 404), (UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "devtable", 404),
(UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "freshuser", 404), (UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "freshuser", 404),
(UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "reader", 404), (UserAuthorization, "GET", {"access_token_uuid": "fake"}, None, "reader", 404),
(UserAssignedAuthorizations, "GET", None, None, None, 401),
(UserAssignedAuthorizations, "GET", None, None, "devtable", 200),
(UserAssignedAuthorizations, "GET", None, None, "freshuser", 200),
(UserAssignedAuthorizations, "GET", None, None, "reader", 200),
(UserAssignedAuthorization, "DELETE", {"assigned_authorization_uuid": "fake"}, None, None, 401),
(
UserAssignedAuthorization,
"DELETE",
{"assigned_authorization_uuid": "fake"},
None,
"devtable",
404,
),
(
UserAssignedAuthorization,
"DELETE",
{"assigned_authorization_uuid": "fake"},
None,
"freshuser",
404,
),
(
UserAssignedAuthorization,
"DELETE",
{"assigned_authorization_uuid": "fake"},
None,
"reader",
404,
),
(UserAggregateLogs, "GET", None, None, None, 401), (UserAggregateLogs, "GET", None, None, None, 401),
(UserAggregateLogs, "GET", None, None, "devtable", 200), (UserAggregateLogs, "GET", None, None, "devtable", 200),
(UserAggregateLogs, "GET", None, None, "freshuser", 200), (UserAggregateLogs, "GET", None, None, "freshuser", 200),

View File

@ -34,6 +34,8 @@ from auth.permissions import (
from data import model from data import model
from data.billing import get_plan from data.billing import get_plan
from data.database import Repository as RepositoryTable from data.database import Repository as RepositoryTable
from data.model.notification import delete_notifications_by_kind
from data.model.oauth import get_assigned_authorization_for_user
from data.users.shared import can_create_user from data.users.shared import can_create_user
from endpoints.api import ( from endpoints.api import (
ApiResource, ApiResource,
@ -1188,6 +1190,28 @@ def authorization_view(access_token):
} }
def assigned_authorization_view(assigned_authorization):
oauth_app = assigned_authorization.application
app_email = oauth_app.avatar_email or oauth_app.organization.email
return {
"application": {
"name": oauth_app.name,
"clientId": oauth_app.client_id,
"description": oauth_app.description,
"url": oauth_app.application_uri,
"avatar": avatar.get_data(oauth_app.name, app_email, "app"),
"organization": {
"name": oauth_app.organization.username,
"avatar": avatar.get_data_for_org(oauth_app.organization),
},
},
"uuid": assigned_authorization.uuid,
"redirectUri": assigned_authorization.redirect_uri,
"scopes": scopes.get_scope_information(assigned_authorization.scope),
"responseType": assigned_authorization.response_type,
}
@resource("/v1/user/authorizations") @resource("/v1/user/authorizations")
@internal_only @internal_only
class UserAuthorizationList(ApiResource): class UserAuthorizationList(ApiResource):
@ -1227,6 +1251,45 @@ class UserAuthorization(ApiResource):
return "", 204 return "", 204
@resource("/v1/user/assignedauthorization")
@show_if(features.ASSIGN_OAUTH_TOKEN)
@internal_only
class UserAssignedAuthorizations(ApiResource):
@require_user_admin()
@nickname("listAssignedAuthorizations")
def get(self):
user = get_authenticated_user()
assignments = model.oauth.list_assigned_authorizations_for_user(user)
# Delete any notifications for assigned authorizations, since they have now been viewed
delete_notifications_by_kind(user, "assigned_authorization")
return {
"authorizations": [
assigned_authorization_view(assignment) for assignment in assignments
]
}
@resource("/v1/user/assignedauthorization/<assigned_authorization_uuid>")
@show_if(features.ASSIGN_OAUTH_TOKEN)
@internal_only
class UserAssignedAuthorization(ApiResource):
@require_user_admin()
@nickname("deleteAssignedAuthorization")
def delete(self, assigned_authorization_uuid):
assigned_authorization = get_assigned_authorization_for_user(
get_authenticated_user(), assigned_authorization_uuid
)
if not assigned_authorization:
raise NotFound()
assigned_authorization.delete_instance()
return "", 204
@resource("/v1/user/starred") @resource("/v1/user/starred")
class StarredRepositoryList(ApiResource): class StarredRepositoryList(ApiResource):
""" """

View File

@ -50,6 +50,13 @@ from buildtrigger.triggerutil import TriggerProviderException
from config import frontend_visible_config from config import frontend_visible_config
from data import model from data import model
from data.database import User, db, random_string_generator from data.database import User, db, random_string_generator
from data.model.oauth import (
assign_token_to_user,
get_oauth_application_for_client_id,
get_token_assignment,
)
from data.model.organization import is_org_admin
from data.model.user import get_nonrobot_user, get_user
from endpoints.api import log_action from endpoints.api import log_action
from endpoints.api.discovery import swagger_route_data from endpoints.api.discovery import swagger_route_data
from endpoints.common import ( from endpoints.common import (
@ -644,6 +651,14 @@ def authorize_application():
response_type = request.form.get("response_type", "code") response_type = request.form.get("response_type", "code")
scope = request.form.get("scope", None) scope = request.form.get("scope", None)
state = request.form.get("state", None) state = request.form.get("state", None)
assignment_uuid = request.form.get("assignment_uuid", None)
# assignment currently only supported for token response type
if response_type != "token" and assignment_uuid is not None:
abort(400)
if not features.ASSIGN_OAUTH_TOKEN and assignment_uuid is not None:
abort(400)
# Add the access token. # Add the access token.
if response_type == "token": if response_type == "token":
@ -651,6 +666,7 @@ def authorize_application():
response_type, response_type,
client_id, client_id,
redirect_uri, redirect_uri,
assignment_uuid,
scope=scope, scope=scope,
state=state, state=state,
) )
@ -705,10 +721,32 @@ def request_authorization_code():
redirect_uri = request.args.get("redirect_uri", None) redirect_uri = request.args.get("redirect_uri", None)
scope = request.args.get("scope", None) scope = request.args.get("scope", None)
state = request.args.get("state", None) state = request.args.get("state", None)
assignment_uuid = request.args.get("assignment_uuid", None)
if not current_user.is_authenticated or not provider.validate_has_scopes( if not get_authenticated_user():
client_id, current_user.db_user().username, scope abort(401)
return
if not features.ASSIGN_OAUTH_TOKEN and assignment_uuid is not None:
abort(400)
# assignment currently only supported for token response type
if response_type != "token" and assignment_uuid is not None:
abort(400)
oauth_app = provider.get_application_for_client_id(client_id)
if not oauth_app:
abort(404)
# check if user is org admin, if not check for user_assignment_id, then check that user belongs that assignment, if none exit with 401
if (
not is_org_admin(current_user.db_user(), oauth_app.organization)
and get_token_assignment(assignment_uuid, current_user.db_user(), oauth_app.organization)
is None
): ):
abort(403)
if not provider.validate_has_scopes(client_id, current_user.db_user().username, scope):
if not provider.validate_redirect_uri(client_id, redirect_uri): if not provider.validate_redirect_uri(client_id, redirect_uri):
current_app = provider.get_application_for_client_id(client_id) current_app = provider.get_application_for_client_id(client_id)
if not current_app: if not current_app:
@ -724,10 +762,7 @@ def request_authorization_code():
abort(404) abort(404)
return return
# Load the application information.
oauth_app = provider.get_application_for_client_id(client_id)
app_email = oauth_app.avatar_email or oauth_app.organization.email app_email = oauth_app.avatar_email or oauth_app.organization.email
oauth_app_view = { oauth_app_view = {
"name": oauth_app.name, "name": oauth_app.name,
"description": oauth_app.description, "description": oauth_app.description,
@ -753,6 +788,7 @@ def request_authorization_code():
scope=scope, scope=scope,
csrf_token_val=generate_csrf_token(), csrf_token_val=generate_csrf_token(),
state=state, state=state,
assignment_uuid=assignment_uuid,
) )
if response_type == "token": if response_type == "token":
@ -760,6 +796,7 @@ def request_authorization_code():
response_type, response_type,
client_id, client_id,
redirect_uri, redirect_uri,
assignment_uuid,
scope=scope, scope=scope,
state=state, state=state,
) )
@ -773,6 +810,49 @@ def request_authorization_code():
) )
@web.route("/oauth/authorize/assignuser", methods=["POST"])
@no_cache
@param_required("client_id")
@param_required("redirect_uri")
@param_required("scope")
@param_required("username")
@process_auth_or_cookie
def assign_user_to_app():
response_type = request.args.get("response_type", "code")
client_id = request.args.get("client_id", None)
redirect_uri = request.args.get("redirect_uri", None)
scope = request.args.get("scope", None)
username = request.args.get("username", None)
if not features.ASSIGN_OAUTH_TOKEN:
abort(404)
if not current_user.is_authenticated:
abort(401)
user = get_nonrobot_user(username)
if not user or not user.enabled:
abort(404)
application = get_oauth_application_for_client_id(client_id)
if not application:
abort(404)
if not is_org_admin(current_user.db_user(), application.organization):
abort(403)
assign_token_to_user(
application,
user,
redirect_uri,
scope,
response_type,
)
return render_page_template_with_routedata(
"message.html", message="Token assigned successfully"
)
@web.route("/oauth/access_token", methods=["POST"]) @web.route("/oauth/access_token", methods=["POST"])
@no_cache @no_cache
@param_required("grant_type", allow_body=True) @param_required("grant_type", allow_body=True)

View File

@ -217,3 +217,4 @@ MANIFEST_SUBJECT_BACKFILL: FeatureNameValue
REFERRERS_API: FeatureNameValue REFERRERS_API: FeatureNameValue
SUPERUSERS_FULL_ACCESS: FeatureNameValue SUPERUSERS_FULL_ACCESS: FeatureNameValue
ASSIGN_OAUTH_TOKEN: FeatureNameValue

View File

@ -74,6 +74,7 @@ from data.model.autoprune import (
create_namespace_autoprune_policy, create_namespace_autoprune_policy,
create_repository_autoprune_policy, create_repository_autoprune_policy,
) )
from data.model.oauth import assign_token_to_user
from data.queue import WorkQueue from data.queue import WorkQueue
from data.registry_model import registry_model from data.registry_model import registry_model
from data.registry_model.datatypes import RepositoryReference from data.registry_model.datatypes import RepositoryReference
@ -531,6 +532,8 @@ def initialize_database():
NotificationKind.create(name="quota_warning") NotificationKind.create(name="quota_warning")
NotificationKind.create(name="quota_error") NotificationKind.create(name="quota_error")
NotificationKind.create(name="assigned_authorization")
QuayRegion.create(name="us") QuayRegion.create(name="us")
QuayService.create(name="quay") QuayService.create(name="quay")
@ -984,6 +987,9 @@ def populate_database(minimal=False):
"http://localhost:8000/o2c.html", "http://localhost:8000/o2c.html",
client_id="deadbeef", client_id="deadbeef",
) )
assign_token_to_user(
oauth_app_1, new_user_1, "http://localhost:8000/o2c.html", "repo:admin", "token"
)
model.oauth.create_application( model.oauth.create_application(
org, org,

View File

@ -8,14 +8,15 @@
<div class="resource-view" resource="authorizedAppsResource" <div class="resource-view" resource="authorizedAppsResource"
error-message="'Cannot load authorized applications'"></div> error-message="'Cannot load authorized applications'"></div>
<div class="empty" ng-if="!authorizedApps.length"> <div class="empty" ng-if="!authorizedApps.length && !assignedAuthApps.length">
<div class="empty-primary-msg">You have not authorized any external applications.</div> <div class="empty-primary-msg">You have not authorized any external applications.</div>
</div> </div>
<table class="cor-table" ng-if="authorizedApps.length"> <table class="cor-table" ng-if="authorizedApps.length || assignedAuthApps.length">
<thead> <thead>
<td>Application Name</td> <td>Application Name</td>
<td>Authorized Permissions</td> <td>Authorized Permissions</td>
<td ng-if="assignedAuthApps.length > 0">Confirm</td>
<td class="options-col"></td> <td class="options-col"></td>
</thead> </thead>
@ -40,6 +41,9 @@
bs-tooltip> bs-tooltip>
{{ scopeInfo.scope }} {{ scopeInfo.scope }}
</span> </span>
</td>
<td>
</td> </td>
<td class="options-col"> <td class="options-col">
<span class="cor-options-menu"> <span class="cor-options-menu">
@ -49,6 +53,41 @@
</span> </span>
</td> </td>
</tr> </tr>
<tr class="auth-info" ng-repeat="authInfo in assignedAuthApps">
<td>
<span class="avatar" size="24" data="authInfo.application.avatar"></span>
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" ng-safenewtab
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }}
</a>
<span ng-if="!authInfo.application.url"
data-title="{{ authInfo.application.description || authInfo.application.name }}"
bs-tooltip>
{{ authInfo.application.name }}
</span>
<span class="by">{{ authInfo.application.organization.name }}</span>
</td>
<td>
<span class="label label-default scope"
ng-class="{'repo:admin': 'label-primary', 'repo:write': 'label-success', 'repo:create': 'label-success'}[scopeInfo.scope]"
ng-repeat="scopeInfo in authInfo.scopes" data-title="{{ scopeInfo.description }}"
bs-tooltip>
{{ scopeInfo.scope }}
</span>
</td>
<td>
<a target="_blank" rel="noopener noreferrer" href="{{ getAuthorizationUrl(authInfo) }}">
Authorize Application
</a>
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="deleteAssignedAuthorization(authInfo)">
<i class="fa fa-times"></i> Delete Authorization
</span>
</span>
</td>
</tr>
</table> </table>
</div> </div>
</div> </div>

View File

@ -12,10 +12,11 @@ angular.module('quay').directive('authorizedAppsManager', function () {
'user': '=user', 'user': '=user',
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, ApiService) { controller: function($scope, $element, ApiService, Config) {
$scope.$watch('isEnabled', function(enabled) { $scope.$watch('isEnabled', function(enabled) {
if (!enabled) { return; } if (!enabled) { return; }
loadAuthedApps(); loadAuthedApps();
loadAssignedAuthApps();
}); });
var loadAuthedApps = function() { var loadAuthedApps = function() {
@ -26,6 +27,17 @@ angular.module('quay').directive('authorizedAppsManager', function () {
}); });
}; };
var loadAssignedAuthApps = function(){
if ($scope.assignedAuthAppsResource || !Config.FEATURE_ASSIGN_OAUTH_TOKEN) {
$scope.assignedAuthApps = [];
return;
}
$scope.assignedAuthAppsResource = ApiService.listAssignedAuthorizationsAsResource().get(function(resp) {
$scope.assignedAuthApps = resp['authorizations'];
});
}
$scope.deleteAccess = function(accessTokenInfo) { $scope.deleteAccess = function(accessTokenInfo) {
var params = { var params = {
'access_token_uuid': accessTokenInfo['uuid'] 'access_token_uuid': accessTokenInfo['uuid']
@ -35,7 +47,31 @@ angular.module('quay').directive('authorizedAppsManager', function () {
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1); $scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
}, ApiService.errorDisplay('Could not revoke authorization')); }, ApiService.errorDisplay('Could not revoke authorization'));
}; };
$scope.deleteAssignedAuthorization = function(assignedAuthorization){
if(!Config.FEATURE_ASSIGN_OAUTH_TOKEN){
return;
}
var params = {
'assigned_authorization_uuid': assignedAuthorization['uuid']
};
ApiService.deleteAssignedAuthorization(null, params).then(function(resp) {
$scope.assignedAuthApps.splice($scope.assignedAuthApps.indexOf(assignedAuthorization), 1);
}, ApiService.errorDisplay('Could not revoke assigned authorization'));
}
$scope.getAuthorizationUrl = function(assignedAuthorization){
let scopes = assignedAuthorization.scopes.map((scope) => scope.scope).join(' ');
let url = "/oauth/authorize?";
url += "response_type=" + assignedAuthorization.responseType;
url += "&client_id=" + assignedAuthorization.application.clientId;
url += "&scope=" + scopes;
url += "&redirect_uri=" + assignedAuthorization.redirectUri;
url += "&assignment_uuid=" + assignedAuthorization.uuid;
return Config.getUrl(url);
}
} }
}; };
return directiveDefinitionObject; return directiveDefinitionObject;
}); });

View File

@ -17,9 +17,12 @@
$scope.Config = Config; $scope.Config = Config;
$scope.OAuthService = OAuthService; $scope.OAuthService = OAuthService;
$scope.updating = false; $scope.updating = false;
$scope.currentEntity = null;
$scope.selectedUser = null;
$scope.customUser = false;
$scope.genScopes = {}; $scope.genScopes = {};
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
$scope.getScopes = function(scopes) { $scope.getScopes = function(scopes) {
@ -92,6 +95,48 @@
}, ApiService.errorDisplay('Could not reset client secret')); }, ApiService.errorDisplay('Could not reset client secret'));
}; };
$scope.generateUrl = function() {
if($scope.application == null){
return "";
}
var base = $scope.selectedUser !== null ? '/oauth/authorize/assignuser?username=' + $scope.selectedUser.name + '&' : '/oauth/authorize?' ;
var url = base + 'response_type=token&client_id=' + $scope.application.client_id + '&scope=' + $scope.getScopes($scope.genScopes).join(' ') + '&redirect_uri=' + Config.getUrl(Config['LOCAL_OAUTH_HANDLER']);
return Config.getUrl(url);
}
$scope.setSelectedUser = function(entity){
$scope.selectedUser = entity;
}
$scope.getScopeInfo = function() {
var selectedScopes = $scope.getScopes($scope.genScopes);
var scopeDetails = [];
for (var i = 0; i < selectedScopes.length; ++i) {
var scope = selectedScopes[i];
if(OAuthService.SCOPES[scope] !== undefined){
var scopeInfo = OAuthService.SCOPES[scope];
scopeInfo.index = i; // Add index for rendering list
scopeDetails.push(scopeInfo)
}
}
return scopeDetails;
}
$scope.hasDangerousScope = function() {
return $scope.getScopeInfo().some(function(scope){
return scope.dangerous;
})
}
$scope.confirmAssignUser = function(){
$('#confirmAssignAuthorizationModal').modal({});
}
$scope.closeConfirmAssignUser = function(){
$('#confirmAssignAuthorizationModal').modal('hide');
}
var loadOrganization = function() { var loadOrganization = function() {
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org; $scope.organization = org;
@ -121,4 +166,4 @@
loadOrganization(); loadOrganization();
loadApplicationInfo(); loadApplicationInfo();
} }
})(); })();

View File

@ -244,7 +244,15 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
'page': function(metadata) { 'page': function(metadata) {
return '/superuser/?tab=servicekeys'; return '/superuser/?tab=servicekeys';
}, },
} },
'assigned_authorization': {
'level': 'primary',
'message': 'You have been assigned an Oauth authorization. Please approve or deny the request.',
'page': function(metadata) {
return '/user/'+metadata["username"]+'/?tab=external';
},
'dismissable': true
},
}; };
notificationService.dismissNotification = function(notification) { notificationService.dismissNotification = function(notification) {

View File

@ -100,11 +100,29 @@
<div style="margin-bottom: 10px"> <div style="margin-bottom: 10px">
Click the button below to generate a new <a href="http://tools.ietf.org/html/rfc6749#section-1.4" target="_new">OAuth 2 Access Token</a>. Click the button below to generate a new <a href="http://tools.ietf.org/html/rfc6749#section-1.4" target="_new">OAuth 2 Access Token</a>.
</div> </div>
<div> <div>
The generated token will act on behalf of user The generated token will act on behalf of user
<span class="avatar" data="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;"></span> <span class="avatar" data="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;" ng-if="!customUser"></span>
<span class="user-name">{{ user.username }}</span> <span class="user-name" ng-if="!customUser">{{ user.username }}</span>
<span class="user-name" ng-if="customUser">
<div class="entity-search"
namespace="organization.name"
placeholder="'User'"
entity-selected="setSelectedUser(entity)"
current-entity="selectedUser"
allowed-entities="['user']"
pull-right="true"></div>
</span>
<span ng-show="Config.FEATURE_ASSIGN_OAUTH_TOKEN">
<button class="btn btn-primary" ng-click="customUser = !customUser">
<div ng-if="!customUser">
Assign another user
</div>
<div ng-if="customUser">
Cancel
</div>
</button>
</span>
</div> </div>
</div> </div>
@ -120,10 +138,13 @@
</table> </table>
<a class="btn btn-success" <a class="btn btn-success"
href="{{ Config.getUrl('/oauth/authorize?response_type=token&client_id=' + application.client_id + '&scope=' + getScopes(genScopes).join(' ') + '&redirect_uri=' + Config.getUrl(Config['LOCAL_OAUTH_HANDLER'])) }}" href="{{ generateUrl() }}"
ng-disabled="!getScopes(genScopes).length" ng-safenewtab> ng-disabled="!getScopes(genScopes).length" ng-safenewtab ng-if="!customUser">
Generate Access Token Generate Access Token
</a> </a>
<button class="btn btn-success" ng-click="confirmAssignUser()" ng-disabled="selectedUser == null" ng-if="customUser">
Assign token
</button>
</cor-tab-pane> </cor-tab-pane>
<!-- OAuth tab --> <!-- OAuth tab -->
@ -184,3 +205,56 @@
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmAssignAuthorizationModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Assign Authorization?</h4>
</div>
<div class="modal-body">
<div class="auth-scopes">
<div ng-if="hasDangerousScope()" class="alert alert-danger">Dangerous scopes will be granted to <b><a class="entity-name" ng-safenewtab href="/user/{{selectedUser.name}}"><i class="fa fa-user"></i>{{selectedUser.name}}</a></b>. Please ensure the scopes and the user are correct.</div>
<div class="reason">This will prompt user <b><a class="entity-name" ng-safenewtab href="/user/{{selectedUser.name}}"><i class="fa fa-user"></i>{{selectedUser.name}}</a></b> to generate a token with the following permissions:</div>
<div class="panel-group">
<div class="scope panel panel-default {{ scope.dangerous && 'dangerous'}}" ng-repeat="scope in getScopeInfo() track by $index">
<div class="panel-heading">
<h4 class="panel-title">
<div class="title-container">
<div class="title {{ !scope.dangerous && 'dangerous'}}" data-toggle="collapse"
data-parent="#scopeGroup" data-target="#description-{{ scope.index }}">
<i class="fa arrow"></i>
<i class="fa {{ scope.icon }} fa-lg"></i>
<a data-toggle="collapse" href="#collapseOne">
{{ scope.scope }}
</a>
<i ng-if="scope.dangerous" class="fa fa-lg fa-exclamation-triangle"
data-title="This scope grants permissions which are potentially dangerous. Be careful when authorizing it!"
data-container="body" bs-tooltip></i>
</div>
</div>
</h4>
</div>
<div id="description-{{ scope.index }}" class="panel-collapse {{ !scope.dangerous ? 'collapse' : 'in'}}">
<div class="panel-body">
<div ng-if="scope.dangerous" class="alert alert-warning">This scope grants permissions which are potentially dangerous. Be careful when authorizing it!</div>
{{ scope.description }}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<form ng-if="selectedUser != null" method="post" action="{{ generateUrl() }}" ng-safenewtab>
<button type="submit" class="btn btn-success">
Assign token
</button>
<button type="button" class="btn btn-default" ng-click="closeConfirmAssignUser()">Close</button>
</form>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View File

@ -72,6 +72,9 @@
<input type="hidden" name="scope" value="{{ scope }}"> <input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}"> <input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}">
<input type="hidden" name="state" value="{{ state }}"> <input type="hidden" name="state" value="{{ state }}">
{% if assignment_uuid %}
<input type="hidden" name="assignment_uuid" value="{{ assignment_uuid }}">
{% endif %}
<button type="submit" class="btn btn-success">Authorize Application</button> <button type="submit" class="btn btn-success">Authorize Application</button>
</form> </form>
{% endif %} {% endif %}
@ -106,6 +109,7 @@
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}"> <input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<input type="hidden" name="scope" value="{{ scope }}"> <input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}"> <input type="hidden" name="_csrf_token" value="{{ csrf_token_val }}">
<input type="hidden" name="assignment_uuid" value="{{ assignment_uuid }}">
<button type="submit" class="btn btn-success">Authorize Application</button> <button type="submit" class="btn btn-success">Authorize Application</button>
</form> </form>
<form method="post" action="/oauth/denyapp" style="display: inline-block"> <form method="post" action="/oauth/denyapp" style="display: inline-block">

View File

@ -25,11 +25,14 @@ from app import (
notification_queue, notification_queue,
storage, storage,
) )
from auth.scopes import READ_REPO, get_scope_information
from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.basehandler import BuildTriggerHandler
from data import database, model from data import database, model
from data.database import Repository as RepositoryTable from data.database import Repository as RepositoryTable
from data.database import RepositoryActionCount from data.database import RepositoryActionCount
from data.logs_model import logs_model from data.logs_model import logs_model
from data.model.organization import create_organization
from data.model.user import get_user
from data.registry_model import registry_model from data.registry_model import registry_model
from endpoints.api import api, api_bp from endpoints.api import api, api_bp
from endpoints.api.billing import ( from endpoints.api.billing import (
@ -138,6 +141,8 @@ from endpoints.api.user import (
StarredRepository, StarredRepository,
StarredRepositoryList, StarredRepositoryList,
User, User,
UserAssignedAuthorization,
UserAssignedAuthorizations,
UserAuthorization, UserAuthorization,
UserAuthorizationList, UserAuthorizationList,
UserNotification, UserNotification,
@ -4599,6 +4604,81 @@ class TestUserAuthorizations(ApiTestCase):
) )
class TestUserAssignedAuthorizations(ApiTestCase):
def test_list_authorizations(self):
assigned_scope = READ_REPO.scope
self.login(PUBLIC_USER)
admin = get_user(ADMIN_ACCESS_USER)
assigned_user = get_user(PUBLIC_USER)
org = create_organization("neworg", "neworg@devtable.com", admin)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assigned_authorization = model.oauth.assign_token_to_user(
app, assigned_user, app.redirect_uri, assigned_scope, "token"
)
response = self.getJsonResponse(
UserAssignedAuthorizations,
expected_code=200,
)
assert len(response["authorizations"]) == 1
authorization = response["authorizations"][0]
del authorization["application"]["avatar"]
del authorization["application"]["organization"]["avatar"]
assert authorization == {
"application": {
"name": app.name,
"clientId": app.client_id,
"description": app.description,
"url": app.application_uri,
"organization": {
"name": org.username,
},
},
"uuid": assigned_authorization.uuid,
"redirectUri": assigned_authorization.redirect_uri,
"scopes": get_scope_information(assigned_scope),
"responseType": assigned_authorization.response_type,
}
class TestUserAssignedAuthorization(ApiTestCase):
def test_delete_assigned_authorization(self):
assigned_scope = READ_REPO.scope
self.login(PUBLIC_USER)
admin = get_user(ADMIN_ACCESS_USER)
assigned_user = get_user(PUBLIC_USER)
org = create_organization("neworg", "neworg@devtable.com", admin)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assigned_authorization = model.oauth.assign_token_to_user(
app, assigned_user, app.redirect_uri, assigned_scope, "token"
)
response = self.getJsonResponse(
UserAssignedAuthorizations,
expected_code=200,
)
assert len(response["authorizations"]) == 1
self.deleteEmptyResponse(
UserAssignedAuthorization,
params=dict(assigned_authorization_uuid=assigned_authorization.uuid),
)
response = self.getJsonResponse(
UserAssignedAuthorizations,
expected_code=200,
)
assert len(response["authorizations"]) == 0
def test_delete_assigned_authorization_not_found(self):
self.login(PUBLIC_USER)
self.deleteResponse(
UserAssignedAuthorization,
params=dict(assigned_authorization_uuid="doesnotexist"),
expected_code=404,
)
class TestSuperUserLogs(ApiTestCase): class TestSuperUserLogs(ApiTestCase):
def test_get_logs(self): def test_get_logs(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)

View File

@ -7,7 +7,6 @@ import unittest
import zlib import zlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
from test.helpers import assert_action_logged
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
import jwt import jwt
@ -19,16 +18,21 @@ from parameterized import parameterized, parameterized_class
from app import app from app import app
from data import model from data import model
from data.database import ServiceKeyApprovalType from data.database import NotificationKind, ServiceKeyApprovalType
from data.model.notification import list_notifications
from data.model.oauth import create_user_access_token
from data.model.organization import create_organization, get_organization
from data.model.user import get_user
from endpoints import keyserver from endpoints import keyserver
from endpoints.api import api, api_bp from endpoints.api import api, api_bp
from endpoints.api.user import Signin from endpoints.api.user import Signin
from endpoints.csrf import OAUTH_CSRF_TOKEN_NAME from endpoints.csrf import OAUTH_CSRF_TOKEN_NAME
from endpoints.keyserver import jwk_with_kid from endpoints.keyserver import jwk_with_kid
from endpoints.test.shared import gen_basic_auth from endpoints.test.shared import gen_basic_auth, toggle_feature
from endpoints.web import web as web_bp from endpoints.web import web as web_bp
from endpoints.webhooks import webhooks as webhooks_bp from endpoints.webhooks import webhooks as webhooks_bp
from initdb import finished_database_for_testing, setup_database_for_testing from initdb import finished_database_for_testing, setup_database_for_testing
from test.helpers import assert_action_logged
from util.registry.gzipinputstream import WINDOW_BUFFER_SIZE from util.registry.gzipinputstream import WINDOW_BUFFER_SIZE
from util.security.token import encode_public_private_token from util.security.token import encode_public_private_token
@ -65,11 +69,15 @@ class EndpointTestCase(unittest.TestCase):
def _add_csrf(self, without_csrf): def _add_csrf(self, without_csrf):
parts = urlparse(without_csrf) parts = urlparse(without_csrf)
query = parse_qs(parts[4]) query = parse_qs(parts[4])
for k, v in query.items():
query[k] = v[0]
self._set_csrf() self._set_csrf()
query[CSRF_TOKEN_KEY] = CSRF_TOKEN query[CSRF_TOKEN_KEY] = CSRF_TOKEN
return urlunparse(list(parts[0:4]) + [urlencode(query)] + list(parts[5:])) url = urlunparse(list(parts[0:4]) + [urlencode(query)] + list(parts[5:]))
return url
def _set_csrf(self): def _set_csrf(self):
with self.app.session_transaction() as sess: with self.app.session_transaction() as sess:
@ -128,7 +136,6 @@ class EndpointTestCase(unittest.TestCase):
url = url_for(resource_name, **kwargs) url = url_for(resource_name, **kwargs)
if with_csrf: if with_csrf:
url = self._add_csrf(url) url = self._add_csrf(url)
post_data = None post_data = None
if form: if form:
post_data = form post_data = form
@ -310,6 +317,8 @@ class WebEndpointTestCase(EndpointTestCase):
self.getResponse("web.receipt", expected_code=404) # Will 401 if no user. self.getResponse("web.receipt", expected_code=404) # Will 401 if no user.
def test_request_authorization_code(self): def test_request_authorization_code(self):
self.login("devtable", "password")
# Try for an invalid client. # Try for an invalid client.
self.getResponse( self.getResponse(
"web.request_authorization_code", "web.request_authorization_code",
@ -332,6 +341,101 @@ class WebEndpointTestCase(EndpointTestCase):
expected_code=200, expected_code=200,
) )
def test_request_authorization_code_assignment_id_with_code(self):
self.login("devtable", "password")
org = model.organization.get_organization("buynlarge")
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
self.getResponse(
"web.request_authorization_code",
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="repo:read",
assignment_uuid="assignmentid",
response_type="code",
expected_code=400,
)
def test_request_authorization_code_not_org_admin(self):
self.login("devtable", "password")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
self.getResponse(
"web.request_authorization_code",
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="repo:read",
expected_code=403,
)
def test_request_authorization_code_assigned_authorization(self):
self.login("devtable", "password")
devtable = get_user("devtable")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assignment = model.oauth.assign_token_to_user(
app, devtable, app.redirect_uri, "repo:read", "token"
)
response = self.getResponse(
"web.request_authorization_code",
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="repo:read",
assignment_uuid=assignment.uuid,
response_type="token",
expected_code=200,
)
assert "Are you sure you want to authorize this application?" in str(response)
def test_request_authorization_code_assigned_authorization_disabled(self):
self.login("devtable", "password")
devtable = get_user("devtable")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assignment = model.oauth.assign_token_to_user(
app, devtable, app.redirect_uri, "repo:read", "token"
)
with toggle_feature("ASSIGN_OAUTH_TOKEN", False):
self.getResponse(
"web.request_authorization_code",
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="repo:read",
assignment_uuid=assignment.uuid,
response_type="token",
expected_code=400,
)
def test_request_authorization_code_assigned_authorization_with_existing_scopes(self):
self.login("devtable", "password")
devtable = get_user("devtable")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assignment = model.oauth.assign_token_to_user(
app, devtable, app.redirect_uri, "repo:read", "token"
)
create_user_access_token(devtable, app.client_id, "repo:read")
oauth_tokens = list(model.oauth.list_access_tokens_for_user(devtable))
filtered_tokens = [token for token in oauth_tokens if token.application == app]
assert len(filtered_tokens) == 1
self.getResponse(
"web.request_authorization_code",
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="repo:read",
assignment_uuid=assignment.uuid,
response_type="token",
expected_code=302,
)
assert model.oauth.get_assigned_authorization_for_user(devtable, assignment.uuid) is None
oauth_tokens = list(model.oauth.list_access_tokens_for_user(devtable))
filtered_tokens = [token for token in oauth_tokens if token.application == app]
assert len(filtered_tokens) == 2
def test_build_status_badge(self): def test_build_status_badge(self):
# Try for an invalid repository. # Try for an invalid repository.
self.getResponse("web.build_status_badge", repository="foo/bar", expected_code=404) self.getResponse("web.build_status_badge", repository="foo/bar", expected_code=404)
@ -526,6 +630,101 @@ class OAuthTestCase(EndpointTestCase):
expected_code=401, expected_code=401,
) )
def test_authorize_application_assignment_id_with_code(self):
# Note: Defined in initdb.py
form = {
"client_id": "deadbeef",
"redirect_uri": "http://localhost:8000/o2c.html",
"scope": "user:admin",
"assignment_uuid": "assignmentid",
"response_type": "code",
}
headers = dict(authorization=gen_basic_auth("devtable", "password"))
self.postResponse(
"web.authorize_application",
headers=headers,
form=form,
with_csrf=True,
expected_code=400,
)
def test_authorize_application_not_org_admin(self):
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
form = {
"client_id": app.client_id,
"redirect_uri": app.redirect_uri,
"scope": "user:admin",
"assignment_uuid": "assignmentid",
"response_type": "token",
}
headers = dict(authorization=gen_basic_auth("devtable", "password"))
response = self.postResponse(
"web.authorize_application",
headers=headers,
form=form,
with_csrf=True,
expected_code=302,
)
assert "unauthorized_client" in response.headers["Location"]
def test_authorize_application_assigned_authorization(self):
devtable = get_user("devtable")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assignment = model.oauth.assign_token_to_user(
app, devtable, app.redirect_uri, "repo:read", "token"
)
tokens = list(model.oauth.list_access_tokens_for_user(devtable))
assert len(tokens) == 1
form = {
"client_id": app.client_id,
"redirect_uri": app.redirect_uri,
"scope": "user:admin",
"assignment_uuid": assignment.uuid,
"response_type": "token",
}
headers = dict(authorization=gen_basic_auth("devtable", "password"))
response = self.postResponse(
"web.authorize_application",
headers=headers,
form=form,
with_csrf=True,
expected_code=302,
)
assert len(list(model.oauth.list_access_tokens_for_user(devtable))) == 2
assert model.oauth.get_assigned_authorization_for_user(devtable, assignment.uuid) is None
def test_authorize_application_assigned_authorization_disabled(self):
devtable = get_user("devtable")
user = get_user("randomuser")
org = model.organization.create_organization("testorg", "testorg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assignment = model.oauth.assign_token_to_user(
app, devtable, app.redirect_uri, "repo:read", "token"
)
form = {
"client_id": app.client_id,
"redirect_uri": app.redirect_uri,
"scope": "user:admin",
"assignment_uuid": assignment.uuid,
"response_type": "token",
}
headers = dict(authorization=gen_basic_auth("devtable", "password"))
with toggle_feature("ASSIGN_OAUTH_TOKEN", False):
self.postResponse(
"web.authorize_application",
headers=headers,
form=form,
with_csrf=True,
expected_code=400,
)
@parameterized.expand(["token", "code"]) @parameterized.expand(["token", "code"])
def test_authorize_nocsrf_correctheader(self, response_type): def test_authorize_nocsrf_correctheader(self, response_type):
# Note: Defined in initdb.py # Note: Defined in initdb.py
@ -956,5 +1155,116 @@ class KeyServerTestCase(EndpointTestCase):
) )
class AssignOauthAppTestCase(EndpointTestCase):
def test_assign_user(self):
self.login("devtable", "password")
assigned_user = get_user("randomuser")
org = get_organization("buynlarge")
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
assigned_auths = model.oauth.list_assigned_authorizations_for_user(assigned_user)
assert len(assigned_auths) == 0
notifications = list_notifications(assigned_user)
assert len(notifications) == 0
response = self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=200,
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="user:admin",
username="randomuser",
response_type="token",
)
assert "Token assigned successfully" in response.data.decode("utf-8")
assigned_auths = model.oauth.list_assigned_authorizations_for_user(assigned_user)
assert len(assigned_auths) == 1
assert assigned_auths[0].application == app
notifications = list_notifications(assigned_user)
assert len(notifications) == 1
assert (
notifications[0].kind
== NotificationKind.select()
.where(NotificationKind.name == "assigned_authorization")
.get()
)
def test_assign_user_unauthenticated(self):
org = get_organization("buynlarge")
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=401,
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="user:admin",
username="randomuser",
response_type="token",
)
def test_assign_user_user_does_not_exist(self):
self.login("devtable", "password")
org = get_organization("buynlarge")
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=404,
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="user:admin",
username="doesnotexist",
response_type="token",
)
def test_assign_user_app_does_not_exist(self):
self.login("devtable", "password")
self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=404,
client_id="doesnotexist",
redirect_uri="http://foo/bar/baz",
scope="user:admin",
username="randomuser",
response_type="token",
)
def test_assign_user_not_org_admin(self):
self.login("devtable", "password")
user = get_user("randomuser")
org = create_organization("neworg", "neworg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=403,
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="user:admin",
username="freshuser",
response_type="token",
)
def test_assign_user_disabled(self):
self.login("devtable", "password")
user = get_user("randomuser")
org = create_organization("neworg", "neworg@devtable.com", user)
app = model.oauth.create_application(org, "test", "http://foo/bar", "http://foo/bar/baz")
with toggle_feature("ASSIGN_OAUTH_TOKEN", False):
self.postResponse(
"web.assign_user_to_app",
with_csrf=True,
expected_code=404,
client_id=app.client_id,
redirect_uri=app.redirect_uri,
scope="user:admin",
username="freshuser",
response_type="token",
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -1531,5 +1531,10 @@ CONFIG_SCHEMA = {
"description": "Set minimal security level for new notifications on detected vulnerabilities. Avoids creation of large number of notifications after first index.", "description": "Set minimal security level for new notifications on detected vulnerabilities. Avoids creation of large number of notifications after first index.",
"x-example": "High", "x-example": "High",
}, },
"FEATURE_ASSIGN_OAUTH_TOKEN": {
"type": "boolean",
"description": "Allows organization administrators to assign OAuth tokens to other users",
"x-example": False,
},
}, },
} }

View File

@ -89,6 +89,8 @@ ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS
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_userid;
ALTER TABLE IF EXISTS ONLY public.organizationrhskus DROP CONSTRAINT IF EXISTS fk_organizationrhskus_orgid; 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.oauthauthorizationcode DROP CONSTRAINT IF EXISTS fk_oauthauthorizationcode_application_id_oauthapplication;
ALTER TABLE IF EXISTS ONLY public.oauthassignedtoken DROP CONSTRAINT IF EXISTS fk_oauthassignedtoken_assigned_user_user;
ALTER TABLE IF EXISTS ONLY public.oauthassignedtoken DROP CONSTRAINT IF EXISTS fk_oauthassignedtoken_application_oauthapplication;
ALTER TABLE IF EXISTS ONLY public.oauthapplication DROP CONSTRAINT IF EXISTS fk_oauthapplication_organization_id_user; 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; ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS fk_oauthaccesstoken_authorized_user_id_user;
ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS fk_oauthaccesstoken_application_id_oauthapplication; ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS fk_oauthaccesstoken_application_id_oauthapplication;
@ -319,6 +321,9 @@ DROP INDEX IF EXISTS public.organizationrhskus_subscription_id_org_id;
DROP INDEX IF EXISTS public.organizationrhskus_subscription_id; DROP INDEX IF EXISTS public.organizationrhskus_subscription_id;
DROP INDEX IF EXISTS public.oauthauthorizationcode_code_name; DROP INDEX IF EXISTS public.oauthauthorizationcode_code_name;
DROP INDEX IF EXISTS public.oauthauthorizationcode_application_id; DROP INDEX IF EXISTS public.oauthauthorizationcode_application_id;
DROP INDEX IF EXISTS public.oauthassignedtoken_uuid;
DROP INDEX IF EXISTS public.oauthassignedtoken_assigned_user;
DROP INDEX IF EXISTS public.oauthassignedtoken_application_id;
DROP INDEX IF EXISTS public.oauthapplication_organization_id; DROP INDEX IF EXISTS public.oauthapplication_organization_id;
DROP INDEX IF EXISTS public.oauthapplication_client_id; DROP INDEX IF EXISTS public.oauthapplication_client_id;
DROP INDEX IF EXISTS public.oauthaccesstoken_uuid; DROP INDEX IF EXISTS public.oauthaccesstoken_uuid;
@ -539,6 +544,7 @@ ALTER TABLE IF EXISTS ONLY public.proxycacheconfig DROP CONSTRAINT IF EXISTS pk_
ALTER TABLE IF EXISTS ONLY public.permissionprototype DROP CONSTRAINT IF EXISTS pk_permissionprototype; 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.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.oauthauthorizationcode DROP CONSTRAINT IF EXISTS pk_oauthauthorizationcode;
ALTER TABLE IF EXISTS ONLY public.oauthassignedtoken DROP CONSTRAINT IF EXISTS pk_oauthassignedtoken;
ALTER TABLE IF EXISTS ONLY public.oauthapplication DROP CONSTRAINT IF EXISTS pk_oauthapplication; 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; ALTER TABLE IF EXISTS ONLY public.oauthaccesstoken DROP CONSTRAINT IF EXISTS pk_oauthaccesstoken;
ALTER TABLE IF EXISTS ONLY public.notificationkind DROP CONSTRAINT IF EXISTS pk_notificationkind; ALTER TABLE IF EXISTS ONLY public.notificationkind DROP CONSTRAINT IF EXISTS pk_notificationkind;
@ -645,6 +651,7 @@ 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.permissionprototype ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.organizationrhskus 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.oauthauthorizationcode ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.oauthassignedtoken ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.oauthapplication 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; ALTER TABLE IF EXISTS public.oauthaccesstoken ALTER COLUMN id DROP DEFAULT;
ALTER TABLE IF EXISTS public.notificationkind ALTER COLUMN id DROP DEFAULT; ALTER TABLE IF EXISTS public.notificationkind ALTER COLUMN id DROP DEFAULT;
@ -804,6 +811,8 @@ DROP SEQUENCE IF EXISTS public.organizationrhskus_id_seq;
DROP TABLE IF EXISTS public.organizationrhskus; DROP TABLE IF EXISTS public.organizationrhskus;
DROP SEQUENCE IF EXISTS public.oauthauthorizationcode_id_seq; DROP SEQUENCE IF EXISTS public.oauthauthorizationcode_id_seq;
DROP TABLE IF EXISTS public.oauthauthorizationcode; DROP TABLE IF EXISTS public.oauthauthorizationcode;
DROP SEQUENCE IF EXISTS public.oauthassignedtoken_id_seq;
DROP TABLE IF EXISTS public.oauthassignedtoken;
DROP SEQUENCE IF EXISTS public.oauthapplication_id_seq; DROP SEQUENCE IF EXISTS public.oauthapplication_id_seq;
DROP TABLE IF EXISTS public.oauthapplication; DROP TABLE IF EXISTS public.oauthapplication;
DROP SEQUENCE IF EXISTS public.oauthaccesstoken_id_seq; DROP SEQUENCE IF EXISTS public.oauthaccesstoken_id_seq;
@ -2836,6 +2845,45 @@ ALTER TABLE public.oauthapplication_id_seq OWNER TO quay;
ALTER SEQUENCE public.oauthapplication_id_seq OWNED BY public.oauthapplication.id; ALTER SEQUENCE public.oauthapplication_id_seq OWNED BY public.oauthapplication.id;
--
-- Name: oauthassignedtoken; Type: TABLE; Schema: public; Owner: quay
--
CREATE TABLE public.oauthassignedtoken (
id integer NOT NULL,
uuid character varying(255) NOT NULL,
assigned_user_id integer NOT NULL,
application_id integer NOT NULL,
redirect_uri character varying(255),
scope character varying(255) NOT NULL,
response_type character varying(255)
);
ALTER TABLE public.oauthassignedtoken OWNER TO quay;
--
-- Name: oauthassignedtoken_id_seq; Type: SEQUENCE; Schema: public; Owner: quay
--
CREATE SEQUENCE public.oauthassignedtoken_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE public.oauthassignedtoken_id_seq OWNER TO quay;
--
-- Name: oauthassignedtoken_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: quay
--
ALTER SEQUENCE public.oauthassignedtoken_id_seq OWNED BY public.oauthassignedtoken.id;
-- --
-- Name: oauthauthorizationcode; Type: TABLE; Schema: public; Owner: quay -- Name: oauthauthorizationcode; Type: TABLE; Schema: public; Owner: quay
-- --
@ -5226,6 +5274,13 @@ ALTER TABLE ONLY public.oauthaccesstoken ALTER COLUMN id SET DEFAULT nextval('pu
ALTER TABLE ONLY public.oauthapplication ALTER COLUMN id SET DEFAULT nextval('public.oauthapplication_id_seq'::regclass); ALTER TABLE ONLY public.oauthapplication ALTER COLUMN id SET DEFAULT nextval('public.oauthapplication_id_seq'::regclass);
--
-- Name: oauthassignedtoken id; Type: DEFAULT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.oauthassignedtoken ALTER COLUMN id SET DEFAULT nextval('public.oauthassignedtoken_id_seq'::regclass);
-- --
-- Name: oauthauthorizationcode id; Type: DEFAULT; Schema: public; Owner: quay -- Name: oauthauthorizationcode id; Type: DEFAULT; Schema: public; Owner: quay
-- --
@ -5627,7 +5682,7 @@ COPY public.accesstokenkind (id, name) FROM stdin;
-- --
COPY public.alembic_version (version_num) FROM stdin; COPY public.alembic_version (version_num) FROM stdin;
0cdd1f27a450 0988213e0885
\. \.
@ -6498,6 +6553,7 @@ COPY public.notificationkind (id, name) FROM stdin;
16 repo_mirror_sync_failed 16 repo_mirror_sync_failed
17 quota_warning 17 quota_warning
18 quota_error 18 quota_error
19 assigned_authorization
\. \.
@ -6517,6 +6573,14 @@ COPY public.oauthapplication (id, client_id, redirect_uri, application_uri, orga
\. \.
--
-- Data for Name: oauthassignedtoken; Type: TABLE DATA; Schema: public; Owner: quay
--
COPY public.oauthassignedtoken (id, uuid, assigned_user_id, application_id, redirect_uri, scope, response_type) FROM stdin;
\.
-- --
-- Data for Name: oauthauthorizationcode; Type: TABLE DATA; Schema: public; Owner: quay -- Data for Name: oauthauthorizationcode; Type: TABLE DATA; Schema: public; Owner: quay
-- --
@ -8248,7 +8312,7 @@ SELECT pg_catalog.setval('public.notification_id_seq', 2, true);
-- Name: notificationkind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: notificationkind_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
SELECT pg_catalog.setval('public.notificationkind_id_seq', 18, true); SELECT pg_catalog.setval('public.notificationkind_id_seq', 19, true);
-- --
@ -8265,6 +8329,13 @@ SELECT pg_catalog.setval('public.oauthaccesstoken_id_seq', 1, false);
SELECT pg_catalog.setval('public.oauthapplication_id_seq', 1, false); SELECT pg_catalog.setval('public.oauthapplication_id_seq', 1, false);
--
-- Name: oauthassignedtoken_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
--
SELECT pg_catalog.setval('public.oauthassignedtoken_id_seq', 1, false);
-- --
-- Name: oauthauthorizationcode_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay -- Name: oauthauthorizationcode_id_seq; Type: SEQUENCE SET; Schema: public; Owner: quay
-- --
@ -9059,6 +9130,14 @@ ALTER TABLE ONLY public.oauthapplication
ADD CONSTRAINT pk_oauthapplication PRIMARY KEY (id); ADD CONSTRAINT pk_oauthapplication PRIMARY KEY (id);
--
-- Name: oauthassignedtoken pk_oauthassignedtoken; Type: CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.oauthassignedtoken
ADD CONSTRAINT pk_oauthassignedtoken PRIMARY KEY (id);
-- --
-- Name: oauthauthorizationcode pk_oauthauthorizationcode; Type: CONSTRAINT; Schema: public; Owner: quay -- Name: oauthauthorizationcode pk_oauthauthorizationcode; Type: CONSTRAINT; Schema: public; Owner: quay
-- --
@ -10653,6 +10732,27 @@ CREATE INDEX oauthapplication_client_id ON public.oauthapplication USING btree (
CREATE INDEX oauthapplication_organization_id ON public.oauthapplication USING btree (organization_id); CREATE INDEX oauthapplication_organization_id ON public.oauthapplication USING btree (organization_id);
--
-- Name: oauthassignedtoken_application_id; Type: INDEX; Schema: public; Owner: quay
--
CREATE INDEX oauthassignedtoken_application_id ON public.oauthassignedtoken USING btree (application_id);
--
-- Name: oauthassignedtoken_assigned_user; Type: INDEX; Schema: public; Owner: quay
--
CREATE INDEX oauthassignedtoken_assigned_user ON public.oauthassignedtoken USING btree (assigned_user_id);
--
-- Name: oauthassignedtoken_uuid; Type: INDEX; Schema: public; Owner: quay
--
CREATE UNIQUE INDEX oauthassignedtoken_uuid ON public.oauthassignedtoken USING btree (uuid);
-- --
-- Name: oauthauthorizationcode_application_id; Type: INDEX; Schema: public; Owner: quay -- Name: oauthauthorizationcode_application_id; Type: INDEX; Schema: public; Owner: quay
-- --
@ -12318,6 +12418,22 @@ ALTER TABLE ONLY public.oauthapplication
ADD CONSTRAINT fk_oauthapplication_organization_id_user FOREIGN KEY (organization_id) REFERENCES public."user"(id); ADD CONSTRAINT fk_oauthapplication_organization_id_user FOREIGN KEY (organization_id) REFERENCES public."user"(id);
--
-- Name: oauthassignedtoken fk_oauthassignedtoken_application_oauthapplication; Type: FK CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.oauthassignedtoken
ADD CONSTRAINT fk_oauthassignedtoken_application_oauthapplication FOREIGN KEY (application_id) REFERENCES public.oauthapplication(id);
--
-- Name: oauthassignedtoken fk_oauthassignedtoken_assigned_user_user; Type: FK CONSTRAINT; Schema: public; Owner: quay
--
ALTER TABLE ONLY public.oauthassignedtoken
ADD CONSTRAINT fk_oauthassignedtoken_assigned_user_user FOREIGN KEY (assigned_user_id) REFERENCES public."user"(id);
-- --
-- Name: oauthauthorizationcode fk_oauthauthorizationcode_application_id_oauthapplication; Type: FK CONSTRAINT; Schema: public; Owner: quay -- Name: oauthauthorizationcode fk_oauthauthorizationcode_application_id_oauthapplication; Type: FK CONSTRAINT; Schema: public; Owner: quay
-- --