mirror of
https://github.com/quay/quay.git
synced 2025-04-18 10:44:06 +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:
parent
bc06a3ef36
commit
e4f05583c1
@ -51,6 +51,7 @@ CLIENT_WHITELIST = [
|
||||
"UI_V2_FEEDBACK_FORM",
|
||||
"TERMS_OF_SERVICE_URL",
|
||||
"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
|
||||
ROBOTS_WHITELIST: Optional[List[str]] = []
|
||||
|
||||
FEATURE_ASSIGN_OAUTH_TOKEN = True
|
||||
|
@ -751,6 +751,7 @@ class User(BaseModel):
|
||||
NamespaceAutoPrunePolicy,
|
||||
AutoPruneTaskStatus,
|
||||
RepositoryAutoPrunePolicy,
|
||||
OauthAssignedToken,
|
||||
}
|
||||
| appr_classes
|
||||
| v22_classes
|
||||
@ -2035,6 +2036,19 @@ class RepositoryAutoPrunePolicy(BaseModel):
|
||||
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
|
||||
# to meet length restrictions.
|
||||
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 (
|
||||
OAuthAccessToken,
|
||||
OAuthApplication,
|
||||
OauthAssignedToken,
|
||||
OAuthAuthorizationCode,
|
||||
User,
|
||||
random_string_generator,
|
||||
)
|
||||
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.provider import AuthorizationProvider
|
||||
from util import get_app_url
|
||||
@ -189,15 +191,20 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||
|
||||
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
|
||||
if response_type != "token":
|
||||
err = "unsupported_response_type"
|
||||
return self._make_redirect_error_response(redirect_uri, err)
|
||||
|
||||
# Check for a valid client ID.
|
||||
is_valid_client_id = self.validate_client_id(client_id)
|
||||
if not is_valid_client_id:
|
||||
oauth_application = self.get_application_for_client_id(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"
|
||||
return self._make_redirect_error_response(redirect_uri, err)
|
||||
|
||||
@ -239,6 +246,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||
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 += "#access_token=%s&token_type=%s&expires_in=%s" % (
|
||||
access_token,
|
||||
@ -274,6 +288,13 @@ class DatabaseAuthorizationProvider(AuthorizationProvider):
|
||||
def discard_refresh_token(self, client_id, refresh_token):
|
||||
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):
|
||||
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):
|
||||
application = lookup_application(org, client_id)
|
||||
if not application:
|
||||
return
|
||||
with db_transaction():
|
||||
application = lookup_application(org, client_id)
|
||||
if not application:
|
||||
return
|
||||
|
||||
application.delete_instance(recursive=True, delete_nullable=True)
|
||||
return application
|
||||
OauthAssignedToken.delete().where(OauthAssignedToken.application == application).execute()
|
||||
|
||||
application.delete_instance(recursive=True, delete_nullable=True)
|
||||
return application
|
||||
|
||||
|
||||
def lookup_access_token_by_uuid(token_uuid):
|
||||
@ -378,12 +402,42 @@ def list_access_tokens_for_user(user_obj):
|
||||
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):
|
||||
query = OAuthApplication.select().join(User).where(OAuthApplication.organization == org)
|
||||
|
||||
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):
|
||||
access_token = access_token or random_string_generator(length=40)()
|
||||
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="",
|
||||
)
|
||||
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)
|
||||
except team.UserAlreadyInTeam:
|
||||
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
|
||||
|
||||
from data.model.organization import get_organization, get_organizations
|
||||
from data.model.user import mark_namespace_for_deletion
|
||||
from data.model.organization import get_organization, get_organizations, is_org_admin
|
||||
from data.model.user import get_user, mark_namespace_for_deletion
|
||||
from data.queue import WorkQueue
|
||||
from test.fixtures import *
|
||||
|
||||
|
||||
@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]
|
||||
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 test.fixtures import *
|
||||
from test.helpers import check_transitive_modifications
|
||||
|
||||
import pytest
|
||||
from mock import patch
|
||||
|
||||
from auth.scopes import READ_REPO
|
||||
from data import model
|
||||
from data.database import DeletedNamespace, EmailConfirmation, FederatedLogin, User
|
||||
from data.fields import Credential
|
||||
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.team import add_user_to_team, create_team
|
||||
from data.model.user import (
|
||||
@ -26,6 +30,7 @@ from data.model.user import (
|
||||
get_public_repo_count,
|
||||
get_pull_credentials,
|
||||
get_quay_user_from_federated_login_name,
|
||||
get_user,
|
||||
list_namespace_robots,
|
||||
lookup_robot,
|
||||
mark_namespace_for_deletion,
|
||||
@ -34,6 +39,8 @@ from data.model.user import (
|
||||
verify_robot,
|
||||
)
|
||||
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.timedeltastring import convert_to_timedelta
|
||||
|
||||
@ -96,7 +103,7 @@ def test_get_active_users(disabled, deleted, initialized_db):
|
||||
assert user.enabled
|
||||
|
||||
|
||||
def test_mark_namespace_for_deletion(initialized_db):
|
||||
def test_mark_user_for_deletion(initialized_db):
|
||||
def create_transaction(db):
|
||||
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.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.
|
||||
queue = WorkQueue("testgcnamespace", create_transaction)
|
||||
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()
|
||||
)
|
||||
|
||||
# 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.
|
||||
new_user = create_user_noverify("foobar", "foo@example.com", email_required=False)
|
||||
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"
|
||||
|
||||
|
||||
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 create_transaction(db):
|
||||
return db.transaction()
|
||||
|
@ -19,6 +19,7 @@ from data.database import (
|
||||
NamespaceAutoPrunePolicy,
|
||||
NamespaceGeoRestriction,
|
||||
OAuthApplication,
|
||||
OauthAssignedToken,
|
||||
QuotaNamespaceSize,
|
||||
RepoMirrorConfig,
|
||||
Repository,
|
||||
@ -1355,6 +1356,7 @@ def _delete_user_linked_data(user):
|
||||
# Delete any OAuth approvals and tokens associated with the user.
|
||||
with db_transaction():
|
||||
for app in OAuthApplication.select().where(OAuthApplication.organization == user):
|
||||
OauthAssignedToken.delete().where(OauthAssignedToken.application == app).execute()
|
||||
app.delete_instance(recursive=True)
|
||||
else:
|
||||
# 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
|
||||
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):
|
||||
"""
|
||||
|
@ -5075,6 +5075,35 @@ SECURITY_TESTS: List[
|
||||
(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, "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, "devtable", 200),
|
||||
(UserAggregateLogs, "GET", None, None, "freshuser", 200),
|
||||
|
@ -34,6 +34,8 @@ from auth.permissions import (
|
||||
from data import model
|
||||
from data.billing import get_plan
|
||||
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 endpoints.api import (
|
||||
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")
|
||||
@internal_only
|
||||
class UserAuthorizationList(ApiResource):
|
||||
@ -1227,6 +1251,45 @@ class UserAuthorization(ApiResource):
|
||||
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")
|
||||
class StarredRepositoryList(ApiResource):
|
||||
"""
|
||||
|
@ -50,6 +50,13 @@ from buildtrigger.triggerutil import TriggerProviderException
|
||||
from config import frontend_visible_config
|
||||
from data import model
|
||||
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.discovery import swagger_route_data
|
||||
from endpoints.common import (
|
||||
@ -644,6 +651,14 @@ def authorize_application():
|
||||
response_type = request.form.get("response_type", "code")
|
||||
scope = request.form.get("scope", 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.
|
||||
if response_type == "token":
|
||||
@ -651,6 +666,7 @@ def authorize_application():
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
assignment_uuid,
|
||||
scope=scope,
|
||||
state=state,
|
||||
)
|
||||
@ -705,10 +721,32 @@ def request_authorization_code():
|
||||
redirect_uri = request.args.get("redirect_uri", None)
|
||||
scope = request.args.get("scope", 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(
|
||||
client_id, current_user.db_user().username, scope
|
||||
if not get_authenticated_user():
|
||||
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):
|
||||
current_app = provider.get_application_for_client_id(client_id)
|
||||
if not current_app:
|
||||
@ -724,10 +762,7 @@ def request_authorization_code():
|
||||
abort(404)
|
||||
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
|
||||
|
||||
oauth_app_view = {
|
||||
"name": oauth_app.name,
|
||||
"description": oauth_app.description,
|
||||
@ -753,6 +788,7 @@ def request_authorization_code():
|
||||
scope=scope,
|
||||
csrf_token_val=generate_csrf_token(),
|
||||
state=state,
|
||||
assignment_uuid=assignment_uuid,
|
||||
)
|
||||
|
||||
if response_type == "token":
|
||||
@ -760,6 +796,7 @@ def request_authorization_code():
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
assignment_uuid,
|
||||
scope=scope,
|
||||
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"])
|
||||
@no_cache
|
||||
@param_required("grant_type", allow_body=True)
|
||||
|
@ -217,3 +217,4 @@ MANIFEST_SUBJECT_BACKFILL: FeatureNameValue
|
||||
REFERRERS_API: FeatureNameValue
|
||||
|
||||
SUPERUSERS_FULL_ACCESS: FeatureNameValue
|
||||
ASSIGN_OAUTH_TOKEN: FeatureNameValue
|
||||
|
@ -74,6 +74,7 @@ from data.model.autoprune import (
|
||||
create_namespace_autoprune_policy,
|
||||
create_repository_autoprune_policy,
|
||||
)
|
||||
from data.model.oauth import assign_token_to_user
|
||||
from data.queue import WorkQueue
|
||||
from data.registry_model import registry_model
|
||||
from data.registry_model.datatypes import RepositoryReference
|
||||
@ -531,6 +532,8 @@ def initialize_database():
|
||||
NotificationKind.create(name="quota_warning")
|
||||
NotificationKind.create(name="quota_error")
|
||||
|
||||
NotificationKind.create(name="assigned_authorization")
|
||||
|
||||
QuayRegion.create(name="us")
|
||||
QuayService.create(name="quay")
|
||||
|
||||
@ -984,6 +987,9 @@ def populate_database(minimal=False):
|
||||
"http://localhost:8000/o2c.html",
|
||||
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(
|
||||
org,
|
||||
|
@ -8,14 +8,15 @@
|
||||
<div class="resource-view" resource="authorizedAppsResource"
|
||||
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>
|
||||
|
||||
<table class="cor-table" ng-if="authorizedApps.length">
|
||||
<table class="cor-table" ng-if="authorizedApps.length || assignedAuthApps.length">
|
||||
<thead>
|
||||
<td>Application Name</td>
|
||||
<td>Authorized Permissions</td>
|
||||
<td ng-if="assignedAuthApps.length > 0">Confirm</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
@ -40,6 +41,9 @@
|
||||
bs-tooltip>
|
||||
{{ scopeInfo.scope }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
@ -49,6 +53,41 @@
|
||||
</span>
|
||||
</td>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,10 +12,11 @@ angular.module('quay').directive('authorizedAppsManager', function () {
|
||||
'user': '=user',
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
controller: function($scope, $element, ApiService, Config) {
|
||||
$scope.$watch('isEnabled', function(enabled) {
|
||||
if (!enabled) { return; }
|
||||
loadAuthedApps();
|
||||
loadAssignedAuthApps();
|
||||
});
|
||||
|
||||
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) {
|
||||
var params = {
|
||||
'access_token_uuid': accessTokenInfo['uuid']
|
||||
@ -35,7 +47,31 @@ angular.module('quay').directive('authorizedAppsManager', function () {
|
||||
$scope.authorizedApps.splice($scope.authorizedApps.indexOf(accessTokenInfo), 1);
|
||||
}, 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;
|
||||
});
|
||||
});
|
||||
|
@ -17,9 +17,12 @@
|
||||
$scope.Config = Config;
|
||||
$scope.OAuthService = OAuthService;
|
||||
$scope.updating = false;
|
||||
|
||||
$scope.currentEntity = null;
|
||||
$scope.selectedUser = null;
|
||||
$scope.customUser = false;
|
||||
$scope.genScopes = {};
|
||||
|
||||
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.getScopes = function(scopes) {
|
||||
@ -92,6 +95,48 @@
|
||||
}, 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() {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
@ -121,4 +166,4 @@
|
||||
loadOrganization();
|
||||
loadApplicationInfo();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
@ -244,7 +244,15 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
||||
'page': function(metadata) {
|
||||
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) {
|
||||
|
@ -100,11 +100,29 @@
|
||||
<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>.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
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="user-name">{{ user.username }}</span>
|
||||
<span class="avatar" data="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;" ng-if="!customUser"></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>
|
||||
|
||||
@ -120,10 +138,13 @@
|
||||
</table>
|
||||
|
||||
<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'])) }}"
|
||||
ng-disabled="!getScopes(genScopes).length" ng-safenewtab>
|
||||
href="{{ generateUrl() }}"
|
||||
ng-disabled="!getScopes(genScopes).length" ng-safenewtab ng-if="!customUser">
|
||||
Generate Access Token
|
||||
</a>
|
||||
<button class="btn btn-success" ng-click="confirmAssignUser()" ng-disabled="selectedUser == null" ng-if="customUser">
|
||||
Assign token
|
||||
</button>
|
||||
</cor-tab-pane>
|
||||
|
||||
<!-- OAuth tab -->
|
||||
@ -184,3 +205,56 @@
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</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="_csrf_token" value="{{ csrf_token_val }}">
|
||||
<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>
|
||||
</form>
|
||||
{% endif %}
|
||||
@ -106,6 +109,7 @@
|
||||
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
|
||||
<input type="hidden" name="scope" value="{{ scope }}">
|
||||
<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>
|
||||
</form>
|
||||
<form method="post" action="/oauth/denyapp" style="display: inline-block">
|
||||
|
@ -25,11 +25,14 @@ from app import (
|
||||
notification_queue,
|
||||
storage,
|
||||
)
|
||||
from auth.scopes import READ_REPO, get_scope_information
|
||||
from buildtrigger.basehandler import BuildTriggerHandler
|
||||
from data import database, model
|
||||
from data.database import Repository as RepositoryTable
|
||||
from data.database import RepositoryActionCount
|
||||
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 endpoints.api import api, api_bp
|
||||
from endpoints.api.billing import (
|
||||
@ -138,6 +141,8 @@ from endpoints.api.user import (
|
||||
StarredRepository,
|
||||
StarredRepositoryList,
|
||||
User,
|
||||
UserAssignedAuthorization,
|
||||
UserAssignedAuthorizations,
|
||||
UserAuthorization,
|
||||
UserAuthorizationList,
|
||||
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):
|
||||
def test_get_logs(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
@ -7,7 +7,6 @@ import unittest
|
||||
import zlib
|
||||
from datetime import datetime, timedelta
|
||||
from io import BytesIO
|
||||
from test.helpers import assert_action_logged
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import jwt
|
||||
@ -19,16 +18,21 @@ from parameterized import parameterized, parameterized_class
|
||||
|
||||
from app import app
|
||||
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.api import api, api_bp
|
||||
from endpoints.api.user import Signin
|
||||
from endpoints.csrf import OAUTH_CSRF_TOKEN_NAME
|
||||
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.webhooks import webhooks as webhooks_bp
|
||||
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.security.token import encode_public_private_token
|
||||
|
||||
@ -65,11 +69,15 @@ class EndpointTestCase(unittest.TestCase):
|
||||
|
||||
def _add_csrf(self, without_csrf):
|
||||
parts = urlparse(without_csrf)
|
||||
|
||||
query = parse_qs(parts[4])
|
||||
for k, v in query.items():
|
||||
query[k] = v[0]
|
||||
|
||||
self._set_csrf()
|
||||
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):
|
||||
with self.app.session_transaction() as sess:
|
||||
@ -128,7 +136,6 @@ class EndpointTestCase(unittest.TestCase):
|
||||
url = url_for(resource_name, **kwargs)
|
||||
if with_csrf:
|
||||
url = self._add_csrf(url)
|
||||
|
||||
post_data = None
|
||||
if form:
|
||||
post_data = form
|
||||
@ -310,6 +317,8 @@ class WebEndpointTestCase(EndpointTestCase):
|
||||
self.getResponse("web.receipt", expected_code=404) # Will 401 if no user.
|
||||
|
||||
def test_request_authorization_code(self):
|
||||
self.login("devtable", "password")
|
||||
|
||||
# Try for an invalid client.
|
||||
self.getResponse(
|
||||
"web.request_authorization_code",
|
||||
@ -332,6 +341,101 @@ class WebEndpointTestCase(EndpointTestCase):
|
||||
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):
|
||||
# Try for an invalid repository.
|
||||
self.getResponse("web.build_status_badge", repository="foo/bar", expected_code=404)
|
||||
@ -526,6 +630,101 @@ class OAuthTestCase(EndpointTestCase):
|
||||
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"])
|
||||
def test_authorize_nocsrf_correctheader(self, response_type):
|
||||
# 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__":
|
||||
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.",
|
||||
"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_orgid;
|
||||
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.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;
|
||||
@ -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.oauthauthorizationcode_code_name;
|
||||
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_client_id;
|
||||
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.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.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.oauthaccesstoken DROP CONSTRAINT IF EXISTS pk_oauthaccesstoken;
|
||||
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.organizationrhskus 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.oauthaccesstoken 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 SEQUENCE IF EXISTS public.oauthauthorizationcode_id_seq;
|
||||
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 TABLE IF EXISTS public.oauthapplication;
|
||||
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;
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -5627,7 +5682,7 @@ COPY public.accesstokenkind (id, name) 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
|
||||
17 quota_warning
|
||||
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
|
||||
--
|
||||
@ -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
|
||||
--
|
||||
|
||||
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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -9059,6 +9130,14 @@ ALTER TABLE ONLY public.oauthapplication
|
||||
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
|
||||
--
|
||||
@ -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);
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
@ -12318,6 +12418,22 @@ ALTER TABLE ONLY public.oauthapplication
|
||||
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
|
||||
--
|
||||
|
Loading…
x
Reference in New Issue
Block a user