mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-05 04:37:29 +01:00
WIP prototype oauth scopes
This commit is contained in:
parent
d4b1344f6a
commit
27e2b64e41
@ -66,14 +66,18 @@ def oauth_start():
|
|||||||
if not client.approved and client.owner != current_user:
|
if not client.approved and client.owner != current_user:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
scope = request.args.get("scope", "public")
|
valid_scopes = {"user:email", "package", "package:release", "package:screenshot"}
|
||||||
if scope != "public":
|
scope = request.args.get("scope", "")
|
||||||
return "Unsupported scope, only public is supported", 400
|
scopes = [x.strip() for x in scope.split(",")]
|
||||||
|
scopes = set([x for x in scopes if x != ""])
|
||||||
|
unknown_scopes = scopes - valid_scopes
|
||||||
|
if unknown_scopes:
|
||||||
|
return f"Unknown scopes: {', '.join(unknown_scopes)}", 400
|
||||||
|
|
||||||
state = request.args.get("state")
|
state = request.args.get("state")
|
||||||
|
|
||||||
token = APIToken.query.filter(APIToken.client == client, APIToken.owner == current_user).first()
|
token = APIToken.query.filter(APIToken.client == client, APIToken.owner == current_user).first()
|
||||||
if token:
|
if token and not (scopes - token.get_scopes()):
|
||||||
token.access_token = random_string(32)
|
token.access_token = random_string(32)
|
||||||
token.auth_code = random_string(32)
|
token.auth_code = random_string(32)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -85,15 +89,19 @@ def oauth_start():
|
|||||||
return redirect(client.redirect_url)
|
return redirect(client.redirect_url)
|
||||||
|
|
||||||
elif action == "authorize":
|
elif action == "authorize":
|
||||||
|
if token is None:
|
||||||
token = APIToken()
|
token = APIToken()
|
||||||
token.access_token = random_string(32)
|
|
||||||
token.name = f"Token for {client.title} by {client.owner.username}"
|
token.name = f"Token for {client.title} by {client.owner.username}"
|
||||||
token.owner = current_user
|
token.owner = current_user
|
||||||
token.client = client
|
token.client = client
|
||||||
|
|
||||||
|
token.access_token = random_string(32)
|
||||||
assert client is not None
|
assert client is not None
|
||||||
token.auth_code = random_string(32)
|
token.auth_code = random_string(32)
|
||||||
db.session.add(token)
|
db.session.add(token)
|
||||||
|
|
||||||
|
token.set_scopes(scopes)
|
||||||
|
|
||||||
add_audit_log(AuditSeverity.USER, current_user,
|
add_audit_log(AuditSeverity.USER, current_user,
|
||||||
f"Granted \"{scope}\" to OAuth2 application \"{client.title}\" by {client.owner.username} [{client_id}] ",
|
f"Granted \"{scope}\" to OAuth2 application \"{client.title}\" by {client.owner.username} [{client_id}] ",
|
||||||
url_for("users.profile", username=current_user.username))
|
url_for("users.profile", username=current_user.username))
|
||||||
@ -102,7 +110,42 @@ def oauth_start():
|
|||||||
|
|
||||||
return redirect(build_redirect_url(client.redirect_url, token.auth_code, state))
|
return redirect(build_redirect_url(client.redirect_url, token.auth_code, state))
|
||||||
|
|
||||||
return render_template("oauth/authorize.html", client=client)
|
scopes_info = []
|
||||||
|
if not scopes:
|
||||||
|
scopes_info.append({
|
||||||
|
"icon": "globe-europe",
|
||||||
|
"title": "Public data only",
|
||||||
|
"description": "Read-only access to your public data",
|
||||||
|
})
|
||||||
|
|
||||||
|
if "user:email" in scopes:
|
||||||
|
scopes_info.append({
|
||||||
|
"icon": "user",
|
||||||
|
"title": gettext("Personal data"),
|
||||||
|
"description": gettext("Email address (read-only)"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if ("package" in scopes or
|
||||||
|
"package:release" in scopes or
|
||||||
|
"package:screenshot" in scopes):
|
||||||
|
if "package" in scopes:
|
||||||
|
msg = gettext("Ability to edit packages and their releases, screenshots, and related data")
|
||||||
|
elif "package:release" in scopes and "package:screenshot" in scopes:
|
||||||
|
msg = gettext("Ability to create and edit releases and screenshots")
|
||||||
|
elif "package:release" in scopes:
|
||||||
|
msg = gettext("Ability to create and edit releases")
|
||||||
|
elif "package:screenshot" in scopes:
|
||||||
|
msg = gettext("Ability to create and edit screenshots")
|
||||||
|
else:
|
||||||
|
assert False, "This should never happen"
|
||||||
|
|
||||||
|
scopes_info.append({
|
||||||
|
"icon": "pen",
|
||||||
|
"title": gettext("Packages"),
|
||||||
|
"description": msg,
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template("oauth/authorize.html", client=client, scopes=scopes_info)
|
||||||
|
|
||||||
|
|
||||||
def error(code: int, msg: str):
|
def error(code: int, msg: str):
|
||||||
|
@ -8,11 +8,6 @@ ContentDB allows you to create an OAuth2 Application and obtain access tokens
|
|||||||
for users.
|
for users.
|
||||||
|
|
||||||
|
|
||||||
## Scopes
|
|
||||||
|
|
||||||
OAuth2 applications can currently only access public user data, using the whoami API.
|
|
||||||
|
|
||||||
|
|
||||||
## Create an OAuth2 Client
|
## Create an OAuth2 Client
|
||||||
|
|
||||||
Go to Settings > [OAuth2 Applications](/user/apps/) > Create
|
Go to Settings > [OAuth2 Applications](/user/apps/) > Create
|
||||||
@ -100,3 +95,12 @@ Next, you should check the access token works by getting the user information:
|
|||||||
curl https://content.minetest.net/api/whoami/ \
|
curl https://content.minetest.net/api/whoami/ \
|
||||||
-H "Authorization: Bearer YOURTOKEN"
|
-H "Authorization: Bearer YOURTOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Scopes
|
||||||
|
|
||||||
|
* (no scope) - public data only
|
||||||
|
* `user:email`: read user email
|
||||||
|
* `package`: write access to packages
|
||||||
|
* `package:release`: create and delete releases
|
||||||
|
* `package:screenshot`: create, edit, delete screenshots
|
||||||
|
6
app/logic/scope.py
Normal file
6
app/logic/scope.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from app.models import APIToken
|
||||||
|
|
||||||
|
|
||||||
|
class Scope:
|
||||||
|
def copy_to_token(self, token: APIToken):
|
||||||
|
pass
|
@ -52,8 +52,38 @@ class APIToken(db.Model):
|
|||||||
client = db.relationship("OAuthClient", foreign_keys=[client_id], back_populates="tokens")
|
client = db.relationship("OAuthClient", foreign_keys=[client_id], back_populates="tokens")
|
||||||
auth_code = db.Column(db.String(34), unique=True, nullable=True)
|
auth_code = db.Column(db.String(34), unique=True, nullable=True)
|
||||||
|
|
||||||
|
scope_user_email = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
scope_package = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
scope_package_release = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
scope_package_screenshot = db.Column(db.Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
def get_scopes(self) -> set[str]:
|
||||||
|
ret = set()
|
||||||
|
if self.scope_user_email:
|
||||||
|
ret.add("user:email")
|
||||||
|
if self.scope_package:
|
||||||
|
ret.add("package")
|
||||||
|
if self.scope_package_release:
|
||||||
|
ret.add("package:release")
|
||||||
|
if self.scope_package_screenshot:
|
||||||
|
ret.add("package:screenshot")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def set_scopes(self, v: set[str]):
|
||||||
|
def pop(key: str):
|
||||||
|
if key in v:
|
||||||
|
v.remove(key)
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.scope_user_email = pop("user:email")
|
||||||
|
self.scope_package = pop("package")
|
||||||
|
self.scope_package_release = pop("package:release") or self.scope_package
|
||||||
|
self.scope_package_screenshot = pop("package:screenshot") or self.scope_package
|
||||||
|
return v
|
||||||
|
|
||||||
def can_operate_on_package(self, package):
|
def can_operate_on_package(self, package):
|
||||||
if self.client is not None:
|
if (self.client is not None and
|
||||||
|
not (self.scope_package or self.scope_package_release or self.scope_package_screenshot)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.package and self.package != package:
|
if self.package and self.package != package:
|
||||||
|
@ -51,19 +51,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for item in scopes %}
|
||||||
<div class="row my-4 align-items-center">
|
<div class="row my-4 align-items-center">
|
||||||
<div class="col-2 text-center fs-3">
|
<div class="col-2 text-center fs-3">
|
||||||
<i class="fas fa-globe-europe"></i>
|
<i class="fas fa-{{ item.icon }}"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p class="my-0">
|
<p class="my-0">
|
||||||
{{ _("Public data only") }}
|
{{ item.title }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-muted my-0">
|
<p class="text-muted my-0">
|
||||||
{{ _("Read-only access to your public data") }}
|
{{ item.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
39
migrations/versions/3d0999440b81_.py
Normal file
39
migrations/versions/3d0999440b81_.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 3d0999440b81
|
||||||
|
Revises: 52cf6746f255
|
||||||
|
Create Date: 2023-11-01 00:45:24.057951
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3d0999440b81'
|
||||||
|
down_revision = '52cf6746f255'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table('api_token', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('scope_user_email', sa.Boolean(), nullable=False, server_default="false"))
|
||||||
|
batch_op.add_column(sa.Column('scope_package', sa.Boolean(), nullable=False, server_default="false"))
|
||||||
|
batch_op.add_column(sa.Column('scope_package_release', sa.Boolean(), nullable=False, server_default="false"))
|
||||||
|
batch_op.add_column(sa.Column('scope_package_screenshot', sa.Boolean(), nullable=False, server_default="false"))
|
||||||
|
|
||||||
|
op.execute("""
|
||||||
|
UPDATE api_token SET
|
||||||
|
scope_user_email = true,
|
||||||
|
scope_package = true,
|
||||||
|
scope_package_release = true,
|
||||||
|
scope_package_screenshot = true;
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table('api_token', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('scope_package_screenshot')
|
||||||
|
batch_op.drop_column('scope_package_release')
|
||||||
|
batch_op.drop_column('scope_package')
|
||||||
|
batch_op.drop_column('scope_user_email')
|
Loading…
Reference in New Issue
Block a user