Skip to content

Commit

Permalink
Merge pull request #97 from arXiv/develop
Browse files Browse the repository at this point in the history
dev -> master
  • Loading branch information
bmaltzan authored Feb 20, 2025
2 parents a39edc8 + cc6796b commit 9778117
Show file tree
Hide file tree
Showing 8 changed files with 704 additions and 484 deletions.
1 change: 1 addition & 0 deletions accounts/accounts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
AUTH_SESSION_COOKIE_NAME = 'ARXIVNG_SESSION_ID'
AUTH_SESSION_COOKIE_DOMAIN = os.environ.get('AUTH_SESSION_COOKIE_DOMAIN', f'.{BASE_SERVER}')
AUTH_SESSION_COOKIE_SECURE = bool(int(os.environ.get('AUTH_SESSION_COOKIE_SECURE', '1')))
MASQUERADE_COOKIE_NAME = 'MASQUERADE'


#################### Classic Auth ####################
Expand Down
3 changes: 2 additions & 1 deletion accounts/accounts/controllers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ def logout(session_cookie: Optional[str],
data = {
'cookies': {
'auth_session_cookie': ('', 0),
'classic_cookie': ('', 0)
'classic_cookie': ('', 0),
'MASQUERADE_COOKIE': ('', 0),
}
}
return data, status.HTTP_303_SEE_OTHER, {'Location': good_next_page(next_page)}
Expand Down
228 changes: 226 additions & 2 deletions accounts/accounts/routes/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@
from accounts.next_page import good_next_page
from accounts.controllers import captcha_image, registration, authentication

# for become_user:
import jwt
import os
import uuid
from arxiv_auth.auth.sessions.store import _generate_nonce
from arxiv_auth.auth.tokens import decode
from arxiv_auth.domain import Session as JWTSession
from arxiv_auth.legacy.cookies import pack, unpack
from arxiv_auth.legacy.models import db, DBSession, DBUserNickname, DBUser
from arxiv_auth.legacy.models import TapirAdminAudit
from arxiv_auth.legacy.util import compute_capabilities, epoch, get_session_duration, now
DEBUG=0


EASTERN = timezone('US/Eastern')

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -56,14 +70,21 @@ def set_cookies(response: Response, data: dict) -> None:
domain = current_app.config['AUTH_SESSION_COOKIE_DOMAIN']
logger.info('Set cookie %s with %s, max_age %s domain %s',
cookie_name, cookie_value, max_age, domain)
params = dict(httponly=True, domain=domain)
is_httponly = False if cookie_name == "MASQUERADE" else True
params = dict(httponly=is_httponly, domain=domain)
if current_app.config['AUTH_SESSION_COOKIE_SECURE']:
# Setting samesite to lax, to allow reasonable links to
# authenticated views using GET requests.
params.update({'secure': True, 'samesite': 'lax'})
response.set_cookie(key=cookie_name, value=cookie_value, max_age=max_age,
**params)

# Not sure unset_masquerade_cookie will be needed,
# as this other code should be enough to clear it first:
# accounts/accounts/controllers/authentication.py:190
def unset_masquerade_cookie(response: Response) -> None:
cookie_name = current_app.config[f'MASQUERADE_COOKIE_NAME']
response.set_cookie(key=cookie_name, value='', max_age=0, httponly=True)

# This is unlikely to be useful once the classic submission UI is disabled.
def unset_submission_cookie(response: Response) -> None:
Expand Down Expand Up @@ -157,7 +178,6 @@ def login() -> Response:
)
return response


@blueprint.route('/logout', methods=['GET'])
def logout() -> Response:
"""Log out of arXiv."""
Expand All @@ -178,6 +198,7 @@ def logout() -> Response:
unset_submission_cookie(response) # Fix for ARXIVNG-1149.
# Partial fix for ARXIVNG-1653, ARXIVNG-1644
unset_permanent_cookie(response)
unset_masquerade_cookie(response)
return response
return redirect(safe_next_page, code=status.HTTP_302_FOUND)

Expand Down Expand Up @@ -206,3 +227,206 @@ def _checked_next_page(otherwise=None) -> str:
return next_page
else:
return otherwise

# Only use post in production to avoid caching issues in fastly,
# but can include GET in dev for testing.
@blueprint.route('/become_user', methods=['POST'])
def become_user_become_user_id() -> Response:

become_user_id = int(request.args.get('become_user_id'))

classic_cookie_name = current_app.config['CLASSIC_COOKIE_NAME']
classic_cookie = request.cookies.get(classic_cookie_name, None)
if DEBUG:
print("BU-DEBUG: classic_cookie", classic_cookie_name, classic_cookie)
classic_cookie_data = unpack(classic_cookie)
if DEBUG:
print("BU-DEBUG: classic_cookie_data", classic_cookie_data)

permanent_cookie_name = current_app.config['CLASSIC_PERMANENT_COOKIE_NAME']
permanent_cookie = request.cookies.get(permanent_cookie_name, None)
if DEBUG:
print("BU-DEBUG: permanent_cookie_name", permanent_cookie_name, permanent_cookie)

session_cookie_name = current_app.config['AUTH_SESSION_COOKIE_NAME']
session_cookie = request.cookies.get(session_cookie_name, None)
if DEBUG:
print("BU-DEBUG: session_cookie_name", session_cookie_name, session_cookie)

session_cookie_domain = current_app.config['AUTH_SESSION_COOKIE_DOMAIN']
if DEBUG:
print("BU-DEBUG: session_cookie_domain", session_cookie_domain)

session_cookie_secure = current_app.config['AUTH_SESSION_COOKIE_SECURE']
if DEBUG:
print("BU-DEBUG: session_cookie_secure", session_cookie_secure)


submit_cookie_name = 'submit_session'
submit_cookie = request.cookies.get(submit_cookie_name, None)
if DEBUG:
print("BU-DEBUG: submit_cookie", submit_cookie_name, submit_cookie)

tracking_cookie_name = os.environ.get('CLASSIC_TRACKING_COOKIE', 'browser')
tracking_cookie = request.cookies.get(tracking_cookie_name, None)
if DEBUG:
print("BU-DEBUG: tracking_cookie", tracking_cookie_name, tracking_cookie)

secret = os.environ.get('JWT_SECRET')

ip_address = request.remote_addr
if DEBUG:
print("BU-DEBUG: ip_address", ip_address)

valid_user = False
jwt_session = None
if session_cookie:

data = jwt.decode(session_cookie, secret, algorithms=["HS256"])
if DEBUG:
print("BU-DEBUG: jwt decode session_cookie:", data)

user_id = f"{ data.get('user_id') }"
if user_id:
user_id = int(user_id)
if user_id > 0:
if DEBUG:
print("BU-DEBUG: jwt user_id", user_id, type(user_id))

admin_user = db.session.query(DBUser) \
.filter(DBUser.user_id == int(user_id)) \
.filter(DBUser.flag_edit_users == 1) \
.filter(DBUser.flag_deleted == 0) \
.filter(DBUser.flag_banned == 0) \
.filter(DBUser.flag_approved == 1) \
.first()

if DEBUG:
print("BU-DEBUG: look for admin_user:", admin_user)
if admin_user:
valid_user = True

valid_become_user_id = False
if valid_user:
if become_user_id > 0:
if DEBUG:
print("BU-DEBUG: become_user_id", become_user_id)
become_user = db.session.query(DBUser) \
.filter(DBUser.user_id == int(become_user_id)) \
.filter(DBUser.flag_edit_users == 0) \
.filter(DBUser.flag_deleted == 0) \
.filter(DBUser.flag_banned == 0) \
.filter(DBUser.flag_approved == 1) \
.first()
#.filter(DBUser.flag_can_lock == 0) \

if DEBUG:
print("BU-DEBUG: become_user", become_user)
if become_user:
valid_become_user_id= True

if DEBUG:
print(dir(become_user))


found_username = False
become_username = None
if valid_become_user_id:
become_user_nickname = db.session.query(DBUserNickname) \
.filter(DBUserNickname.user_id == int(become_user_id)) \
.filter(DBUserNickname.flag_valid == 1) \
.first()
if DEBUG:
print("BU-DEBUG: become_user_nickname", become_user_nickname)
if become_user_nickname:
become_username = become_user_nickname.nickname
found_username = True
if DEBUG:
print("BU-DEBUG: become_username", become_username)

if not (valid_user and valid_become_user_id and found_username):
response = make_response(redirect("/login", code=status.HTTP_303_SEE_OTHER))
return response
else:

start_time = ( datetime.now(tz=UTC) ).replace(microsecond=0)
expires = ( start_time + timedelta(seconds=3600) ).replace(microsecond=0)
now1 = now()
if DEBUG:
print("BU-DEBUG: dates.start_time:", start_time)
print("BU-DEBUG: dates.expires:", expires)
print("BU-DEBUG: dates.now1:", now1)

become_session = DBSession(
end_time=0,
last_reissue=now1,
start_time=now1,
user_id=become_user.user_id,
)
db.session.add(become_session)
db.session.commit()
if DEBUG:
print("BU-DEBUG: become_session", become_session)

admin_audit = TapirAdminAudit(
action="become-user",
admin_user=admin_user.user_id,
affected_user=become_user.user_id,
comment='No-comment',
data=become_session.session_id,
ip_addr=ip_address,
log_date=now1,
session=become_session,
tracking_cookie=tracking_cookie,
)
db.session.add(admin_audit)
db.session.commit()
if DEBUG:
print("BU-DEBUG: admin_audit", admin_audit)

become_jwt_data = {
'user_id': become_session.user_id,
'session_id': str(uuid.uuid4()),
'nonce': _generate_nonce(),
"expires": expires.isoformat(),
"start_time": start_time.isoformat(),
}
become_jwt = jwt.encode(become_jwt_data, secret)
if DEBUG:
print("BU-DEBUG: become_jwt", become_jwt)

next_page = "https://check.dev.arxiv.org/"
data: Dict[str, Any] = {
'next_page': next_page,
'admin_user': admin_user,
'become_user': become_user,
'become_username': become_username,
}
response = Response(
render_template("accounts/become_user.html", **data),
status=200
)

become_session_cookie = pack(
become_session.session_id,
become_session.user_id,
ip_address,
start_time,
compute_capabilities(become_user),
)
if DEBUG:
print("BU-DEBUG: become_session_cookie", become_session_cookie)

data: Dict[str, Any] = {
'cookies': {
'AUTH_SESSION_COOKIE': (become_jwt, 3600),
'CLASSIC_COOKIE': (become_session_cookie, 3600),
'MASQUERADE_COOKIE': ('1', 3600),
}
}
set_cookies(response, data)
unset_submission_cookie(response)
unset_permanent_cookie(response)
response.set_cookie(key=tracking_cookie_name, value='', max_age=0, httponly=True)

return response
21 changes: 21 additions & 0 deletions accounts/accounts/templates/accounts/become_user.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div>
<p>You were admin user:
<ul>
<li>{{ admin_user.user_id }}</li>
<li>{{ admin_user.first_name }}</li>
<li>{{ admin_user.last_name }}</li>
<li>{{ admin_user.email }}</li>
</ul>
</p>
<p>You are now logged in as:
<ul>
<li>{{ become_user.user_id }}</li>
<li>{{ become_user.first_name }}</li>
<li>{{ become_user.last_name }}</li>
<li>{{ become_user.email }}</li>
<li>{{ become_username}}</li>
</ul>
</p>
<a href="{{ next_page }}">{{ next_page }}</a>
</div>

5 changes: 5 additions & 0 deletions accounts/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ redis-py-cluster = "*" # constrained by what is set in arxiv-auth
arxiv-auth = {path = "../arxiv-auth"}
arxiv-base = {git = "https://github.com/arXiv/arxiv-base.git", rev = "1.0.1"}

# I needed to add these for local testing,
# but do not want to commit them and modify what's running at cit.
# Werkzeug = "2.2.3"
# pydantic = "2.1.1"

[tool.poetry.dev-dependencies]
mimesis = "*"
mypy = "*"
Expand Down
45 changes: 44 additions & 1 deletion arxiv-auth/arxiv_auth/legacy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,47 @@ class DBSession(db.Model):
user = relationship('DBUser')


class TapirAdminAudit(db.Model):
'''
mysql> desc tapir_admin_audit;
+-----------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+----------------+
| log_date | int unsigned | NO | MUL | 0 | |
| session_id | int unsigned | YES | MUL | NULL | |
| ip_addr | varchar(16) | NO | MUL | | |
| remote_host | varchar(255) | NO | | | |
| admin_user | int unsigned | YES | MUL | NULL | |
| affected_user | int unsigned | NO | MUL | 0 | |
| tracking_cookie | varchar(255) | NO | | | |
| action | varchar(32) | NO | | | |
| data | text | NO | MUL | NULL | |
| comment | text | NO | | NULL | |
| entry_id | int unsigned | NO | PRI | NULL | auto_increment |
+-----------------+--------------+------+-----+---------+----------------+
'''
__tablename__ = 'tapir_admin_audit'

log_date = Column(Integer, nullable=False, index=True, server_default=text("'0'"))
session_id = Column(ForeignKey('tapir_sessions.session_id'), index=True)
ip_addr = Column(String(16), nullable=False, index=True, server_default=text("''"))
remote_host = Column(String(255), nullable=False, server_default=text("''"))
admin_user = Column(ForeignKey('tapir_users.user_id'), index=True)
affected_user = Column(ForeignKey('tapir_users.user_id'), nullable=False, index=True, server_default=text("'0'"))
tracking_cookie = Column(String(255), nullable=False, server_default=text("''"))
action = Column(String(32), nullable=False, server_default=text("''"))
data = Column(Text, nullable=False, index=True)
comment = Column(Text, nullable=False)
entry_id = Column(Integer, primary_key=True)

#tapir_users = relationship('DBSession', primaryjoin='TapirAdminAudit.admin_user == DBUser.user_id')
#tapir_users1 = relationship('DBSession', primaryjoin='TapirAdminAudit.affected_user == DBUser.user_id')
session = relationship('DBSession')

#tapir_users = relationship('TapirUsers', primaryjoin='TapirAdminAudit.admin_user == TapirUsers.user_id')
#tapir_users1 = relationship('TapirUsers', primaryjoin='TapirAdminAudit.affected_user == TapirUsers.user_id')


class DBSessionsAudit(db.Model):
"""Legacy arXiv session audit table. Notably has a tracking cookie."""

Expand Down Expand Up @@ -92,7 +133,9 @@ class DBUser(db.Model):
flag_html_email = Column(Integer, nullable=False, server_default=text("'0'"))
tracking_cookie = Column(String(255), nullable=False, index=True, server_default=text("''"))
flag_allow_tex_produced = Column(Integer, nullable=False, server_default=text("'0'"))

flag_can_lock = Column(Integer, nullable=False, index=False, server_default=text("'0'"))
def __repr__(self):
return f"{ type(self) }:{ self.user_id }/{ self.first_name}/{ self.last_name}"

class DBPolicyClass(db.Model):
"""Legacy authorization table."""
Expand Down
Loading

0 comments on commit 9778117

Please sign in to comment.