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:
@ -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
|
||||||
|
@ -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 = {
|
||||||
|
@ -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")
|
||||||
|
)
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
)
|
||||||
|
96
data/model/test/test_oauth.py
Normal file
96
data/model/test/test_oauth.py
Normal 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
|
||||||
|
)
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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),
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -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) {
|
||||||
|
@ -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">×</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 -->
|
||||||
|
@ -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">
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
--
|
--
|
||||||
|
Reference in New Issue
Block a user