invenio-oauth2server 5.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invenio_oauth2server/__init__.py +119 -0
- invenio_oauth2server/admin.py +81 -0
- invenio_oauth2server/alembic/12a88921ada2_create_oauth2server_tables.py +96 -0
- invenio_oauth2server/alembic/3d7f57a61d67_change_expires_at_type.py +36 -0
- invenio_oauth2server/alembic/4e57407b8e4a_add_on_delete_cascade.py +122 -0
- invenio_oauth2server/alembic/aa546f2a8d2f_create_oauth2server_branch.py +21 -0
- invenio_oauth2server/cli.py +96 -0
- invenio_oauth2server/config.py +78 -0
- invenio_oauth2server/decorators.py +72 -0
- invenio_oauth2server/errors.py +95 -0
- invenio_oauth2server/ext.py +350 -0
- invenio_oauth2server/forms.py +189 -0
- invenio_oauth2server/models.py +428 -0
- invenio_oauth2server/provider.py +156 -0
- invenio_oauth2server/proxies.py +12 -0
- invenio_oauth2server/scopes.py +16 -0
- invenio_oauth2server/templates/invenio_oauth2server/authorize.html +63 -0
- invenio_oauth2server/templates/invenio_oauth2server/base.html +13 -0
- invenio_oauth2server/templates/invenio_oauth2server/errors.html +33 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/_macros.html +30 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/base.html +13 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/client_new.html +27 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/client_view.html +96 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/helpers.html +58 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/index.html +84 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/token_new.html +28 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/token_permission_view.html +49 -0
- invenio_oauth2server/templates/invenio_oauth2server/settings/token_view.html +50 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/authorize.html +69 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/base.html +13 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/errors.html +28 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/_macros.html +78 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/base.html +13 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/client_new.html +29 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/client_view.html +121 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/helpers.html +118 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/index.html +95 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/token_new.html +30 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/token_permission_view.html +49 -0
- invenio_oauth2server/templates/semantic-ui/invenio_oauth2server/settings/token_view.html +60 -0
- invenio_oauth2server/theme/semantic/form_styling.py +46 -0
- invenio_oauth2server/translations/ar/LC_MESSAGES/messages.po +492 -0
- invenio_oauth2server/translations/bg/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/ca/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/cs/LC_MESSAGES/messages.po +495 -0
- invenio_oauth2server/translations/da/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/de/LC_MESSAGES/messages.po +506 -0
- invenio_oauth2server/translations/el/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/es/LC_MESSAGES/messages.po +498 -0
- invenio_oauth2server/translations/et/LC_MESSAGES/messages.po +495 -0
- invenio_oauth2server/translations/fa/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/fi/LC_MESSAGES/messages.po +496 -0
- invenio_oauth2server/translations/fr/LC_MESSAGES/messages.po +498 -0
- invenio_oauth2server/translations/hr/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/hu/LC_MESSAGES/messages.po +497 -0
- invenio_oauth2server/translations/it/LC_MESSAGES/messages.po +493 -0
- invenio_oauth2server/translations/ja/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/ka/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/ko/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/lt/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/messages.pot +463 -0
- invenio_oauth2server/translations/no/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/pl/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/pt/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/translations/ro/LC_MESSAGES/messages.po +495 -0
- invenio_oauth2server/translations/ru/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/sk/LC_MESSAGES/messages.po +488 -0
- invenio_oauth2server/translations/sv/LC_MESSAGES/messages.po +501 -0
- invenio_oauth2server/translations/tr/LC_MESSAGES/messages.po +498 -0
- invenio_oauth2server/translations/uk/LC_MESSAGES/messages.po +468 -0
- invenio_oauth2server/translations/zh_CN/LC_MESSAGES/messages.po +469 -0
- invenio_oauth2server/translations/zh_TW/LC_MESSAGES/messages.po +467 -0
- invenio_oauth2server/utils.py +65 -0
- invenio_oauth2server/validators.py +60 -0
- invenio_oauth2server/views/__init__.py +13 -0
- invenio_oauth2server/views/server.py +152 -0
- invenio_oauth2server/views/settings.py +268 -0
- invenio_oauth2server-5.0.1.dist-info/METADATA +74 -0
- invenio_oauth2server-5.0.1.dist-info/RECORD +83 -0
- invenio_oauth2server-5.0.1.dist-info/WHEEL +4 -0
- invenio_oauth2server-5.0.1.dist-info/entry_points.txt +35 -0
- invenio_oauth2server-5.0.1.dist-info/licenses/AUTHORS.rst +25 -0
- invenio_oauth2server-5.0.1.dist-info/licenses/LICENSE +20 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015-2024 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2023-2026 Graz University of Technology.
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 KTH Royal Institute of Technology.
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
"""Invenio module that implements OAuth 2 server.
|
|
7
|
+
|
|
8
|
+
Protecting your REST API with authentication
|
|
9
|
+
--------------------------------------------
|
|
10
|
+
|
|
11
|
+
If you want to have your REST API endpoints protected using OAuth you
|
|
12
|
+
should register their blueprint inside the API app
|
|
13
|
+
(InvenioOAuth2ServerREST) which by default includes a ``before_request`` hook.
|
|
14
|
+
This hook will, if there is an OAuth token, verify it and set the current user
|
|
15
|
+
accordingly. It is important to highlight that this configuration allows
|
|
16
|
+
either authenticated clients or anonymous clients.
|
|
17
|
+
|
|
18
|
+
In case you need to allow access to a resource only for authenticated clients,
|
|
19
|
+
you should use the `require_api_auth
|
|
20
|
+
<api.html#invenio_oauth2server.decorators.require_api_auth>`__
|
|
21
|
+
decorator which requires OAuth2 login:
|
|
22
|
+
|
|
23
|
+
.. code-block:: python
|
|
24
|
+
|
|
25
|
+
@app.route('/api/resource', methods=['GET'])
|
|
26
|
+
@require_api_auth()
|
|
27
|
+
def index():
|
|
28
|
+
return 'Protected resource'
|
|
29
|
+
|
|
30
|
+
However, protecting your resources only with authentication is not
|
|
31
|
+
recommended. Instead, you should add an extra layer using always
|
|
32
|
+
`scopes <api.html#invenio_oauth2server.models.Scope>`__. This is because,
|
|
33
|
+
basically, any client that owns a token has control over every user resource.
|
|
34
|
+
Therefore, using scopes gives a fine-grain control. Here an example using
|
|
35
|
+
the default ``email_scope``:
|
|
36
|
+
|
|
37
|
+
.. code-block:: python
|
|
38
|
+
|
|
39
|
+
from invenio_oauth2server.scopes import email_scope
|
|
40
|
+
from flask_login import current_user
|
|
41
|
+
|
|
42
|
+
@app.route('/api/email', methods=['GET'])
|
|
43
|
+
@require_api_auth()
|
|
44
|
+
@require_oauth_scopes(email_scope.id_)
|
|
45
|
+
def index():
|
|
46
|
+
return current_user.email
|
|
47
|
+
|
|
48
|
+
Delegating rights via scopes
|
|
49
|
+
----------------------------
|
|
50
|
+
|
|
51
|
+
As mentioned before, the recommended way to protect your endpoints is to use
|
|
52
|
+
fine-grain control with scopes. Invenio-OAuth2Server offers the possibility to
|
|
53
|
+
create new ones:
|
|
54
|
+
|
|
55
|
+
.. code-block:: python
|
|
56
|
+
|
|
57
|
+
from invenio_oauth2server.models import Scope
|
|
58
|
+
|
|
59
|
+
homepage_read = Scope('homepage:read',
|
|
60
|
+
help_text='Access to the homepage',
|
|
61
|
+
group='test')
|
|
62
|
+
|
|
63
|
+
Next, you should add them to ``setup.py`` entrypoints so they get initialized
|
|
64
|
+
at start up:
|
|
65
|
+
|
|
66
|
+
.. code-block:: python
|
|
67
|
+
|
|
68
|
+
setup(
|
|
69
|
+
...
|
|
70
|
+
entry_points={
|
|
71
|
+
'invenio_oauth2server.scopes': [
|
|
72
|
+
'homepage_read = path.to.scopes.file:homepage_read',
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
...
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
And then, they can be used in your application:
|
|
79
|
+
|
|
80
|
+
.. code-block:: python
|
|
81
|
+
|
|
82
|
+
from path.to.scopes.file import homepage_read
|
|
83
|
+
|
|
84
|
+
@app.route('/', methods=['GET'])
|
|
85
|
+
@require_api_auth()
|
|
86
|
+
@require_oauth_scopes(homepage_read.id_)
|
|
87
|
+
def index():
|
|
88
|
+
return 'Front page content.'
|
|
89
|
+
|
|
90
|
+
So, finally, with this example, we would allow any authenticated client with
|
|
91
|
+
rights to use the ``homepage_scope`` to read the homepage but, prevent from
|
|
92
|
+
reading the email if they do not have rights for using the ``email_scope``.
|
|
93
|
+
|
|
94
|
+
Access control
|
|
95
|
+
--------------
|
|
96
|
+
|
|
97
|
+
It is important to remember that the usage of authentication and scopes is not
|
|
98
|
+
enough in most of the cases so access control need to be configured as well.
|
|
99
|
+
For more information about access control in Invenio you can visit
|
|
100
|
+
`Invenio-Access
|
|
101
|
+
<http://invenio-access.readthedocs.io/en/latest/>`__ documentation.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
from .decorators import require_api_auth, require_oauth_scopes
|
|
107
|
+
from .ext import InvenioOAuth2Server, InvenioOAuth2ServerREST
|
|
108
|
+
from .proxies import current_oauth2server
|
|
109
|
+
|
|
110
|
+
__version__ = "5.0.1"
|
|
111
|
+
|
|
112
|
+
__all__ = (
|
|
113
|
+
"__version__",
|
|
114
|
+
"InvenioOAuth2Server",
|
|
115
|
+
"InvenioOAuth2ServerREST",
|
|
116
|
+
"require_api_auth",
|
|
117
|
+
"require_oauth_scopes",
|
|
118
|
+
"current_oauth2server",
|
|
119
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2016-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Admin views for managing access to actions."""
|
|
6
|
+
|
|
7
|
+
from flask_admin.contrib.sqla import ModelView
|
|
8
|
+
from invenio_db import db
|
|
9
|
+
|
|
10
|
+
from .models import Client, Token
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _(x):
|
|
14
|
+
"""Identity."""
|
|
15
|
+
return x
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ClientView(ModelView):
|
|
19
|
+
"""View for managing access to actions by users."""
|
|
20
|
+
|
|
21
|
+
can_delete = True
|
|
22
|
+
can_create = False
|
|
23
|
+
can_view_details = True
|
|
24
|
+
column_display_all_relations = True
|
|
25
|
+
|
|
26
|
+
list_all = (
|
|
27
|
+
"name",
|
|
28
|
+
"description",
|
|
29
|
+
"website",
|
|
30
|
+
"user_id",
|
|
31
|
+
"client_id",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
column_list = column_searchable_list = column_sortable_list = (
|
|
35
|
+
column_details_list
|
|
36
|
+
) = list_all
|
|
37
|
+
|
|
38
|
+
column_list = list_all
|
|
39
|
+
column_default_sort = ("client_id", True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TokenView(ModelView):
|
|
43
|
+
"""View for managing access to actions by users with given role."""
|
|
44
|
+
|
|
45
|
+
can_delete = True
|
|
46
|
+
can_create = False
|
|
47
|
+
can_view_details = True
|
|
48
|
+
list_all = (
|
|
49
|
+
"id",
|
|
50
|
+
"client_id",
|
|
51
|
+
"user_id",
|
|
52
|
+
"token_type",
|
|
53
|
+
"expires",
|
|
54
|
+
)
|
|
55
|
+
column_list = column_searchable_list = column_sortable_list = (
|
|
56
|
+
column_details_list
|
|
57
|
+
) = list_all
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
oauth2server_clients_adminview = {
|
|
61
|
+
"view_class": ClientView,
|
|
62
|
+
"args": [Client, db.session],
|
|
63
|
+
"kwargs": {
|
|
64
|
+
"name": _("OAuth Applications"),
|
|
65
|
+
"category": _("User Management"),
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
oauth2server_tokens_adminview = {
|
|
70
|
+
"view_class": TokenView,
|
|
71
|
+
"args": [Token, db.session],
|
|
72
|
+
"kwargs": {
|
|
73
|
+
"name": _("OAuth Application Tokens"),
|
|
74
|
+
"category": _("User Management"),
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
__all__ = (
|
|
79
|
+
"oauth2server_clients_adminview",
|
|
80
|
+
"oauth2server_tokens_adminview",
|
|
81
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2016-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Create oauth2server tables."""
|
|
6
|
+
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
import sqlalchemy_utils
|
|
9
|
+
from alembic import op
|
|
10
|
+
|
|
11
|
+
# revision identifiers, used by Alembic.
|
|
12
|
+
revision = "12a88921ada2"
|
|
13
|
+
down_revision = "aa546f2a8d2f"
|
|
14
|
+
branch_labels = ()
|
|
15
|
+
depends_on = "9848d0149abd"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def upgrade():
|
|
19
|
+
"""Upgrade database."""
|
|
20
|
+
op.create_table(
|
|
21
|
+
"oauth2server_client",
|
|
22
|
+
sa.Column("name", sa.String(length=40), nullable=True),
|
|
23
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
24
|
+
sa.Column("website", sqlalchemy_utils.types.url.URLType(), nullable=True),
|
|
25
|
+
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
26
|
+
sa.Column("client_id", sa.String(length=255), nullable=False),
|
|
27
|
+
sa.Column("client_secret", sa.String(length=255), nullable=False),
|
|
28
|
+
sa.Column("is_confidential", sa.Boolean(name="is_confidential"), nullable=True),
|
|
29
|
+
sa.Column("is_internal", sa.Boolean(name="is_internal"), nullable=True),
|
|
30
|
+
sa.Column("_redirect_uris", sa.Text(), nullable=True),
|
|
31
|
+
sa.Column("_default_scopes", sa.Text(), nullable=True),
|
|
32
|
+
sa.ForeignKeyConstraint(
|
|
33
|
+
["user_id"],
|
|
34
|
+
["accounts_user.id"],
|
|
35
|
+
),
|
|
36
|
+
sa.PrimaryKeyConstraint("client_id"),
|
|
37
|
+
)
|
|
38
|
+
op.create_index(
|
|
39
|
+
op.f("ix_oauth2server_client_client_secret"),
|
|
40
|
+
"oauth2server_client",
|
|
41
|
+
["client_secret"],
|
|
42
|
+
unique=True,
|
|
43
|
+
)
|
|
44
|
+
op.create_table(
|
|
45
|
+
"oauth2server_token",
|
|
46
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
47
|
+
sa.Column("client_id", sa.String(length=255), nullable=False),
|
|
48
|
+
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
49
|
+
sa.Column("token_type", sa.String(length=255), nullable=True),
|
|
50
|
+
sa.Column(
|
|
51
|
+
"access_token", sqlalchemy_utils.StringEncryptedType(), nullable=True
|
|
52
|
+
),
|
|
53
|
+
sa.Column(
|
|
54
|
+
"refresh_token", sqlalchemy_utils.StringEncryptedType(), nullable=True
|
|
55
|
+
),
|
|
56
|
+
sa.Column("expires", sa.DateTime(), nullable=True),
|
|
57
|
+
sa.Column("_scopes", sa.Text(), nullable=True),
|
|
58
|
+
sa.Column("is_personal", sa.Boolean(name="is_personal"), nullable=True),
|
|
59
|
+
sa.Column("is_internal", sa.Boolean(name="is_internal"), nullable=True),
|
|
60
|
+
sa.ForeignKeyConstraint(
|
|
61
|
+
["client_id"],
|
|
62
|
+
["oauth2server_client.client_id"],
|
|
63
|
+
),
|
|
64
|
+
sa.ForeignKeyConstraint(
|
|
65
|
+
["user_id"],
|
|
66
|
+
["accounts_user.id"],
|
|
67
|
+
),
|
|
68
|
+
sa.PrimaryKeyConstraint("id"),
|
|
69
|
+
)
|
|
70
|
+
op.create_index(
|
|
71
|
+
"ix_oauth2server_token_access_token",
|
|
72
|
+
"oauth2server_token",
|
|
73
|
+
["access_token"],
|
|
74
|
+
unique=True,
|
|
75
|
+
mysql_length=255,
|
|
76
|
+
)
|
|
77
|
+
op.create_index(
|
|
78
|
+
"ix_oauth2server_token_refresh_token",
|
|
79
|
+
"oauth2server_token",
|
|
80
|
+
["refresh_token"],
|
|
81
|
+
unique=True,
|
|
82
|
+
mysql_length=255,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def downgrade():
|
|
87
|
+
"""Downgrade database."""
|
|
88
|
+
op.drop_index(
|
|
89
|
+
"ix_oauth2server_token_refresh_token", table_name="oauth2server_token"
|
|
90
|
+
)
|
|
91
|
+
op.drop_index("ix_oauth2server_token_access_token", table_name="oauth2server_token")
|
|
92
|
+
op.drop_table("oauth2server_token")
|
|
93
|
+
op.drop_index(
|
|
94
|
+
op.f("ix_oauth2server_client_client_secret"), table_name="oauth2server_client"
|
|
95
|
+
)
|
|
96
|
+
op.drop_table("oauth2server_client")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2017-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Change expires_at type to utc aware datetime."""
|
|
6
|
+
|
|
7
|
+
from alembic import op
|
|
8
|
+
from invenio_db import db
|
|
9
|
+
|
|
10
|
+
# revision identifiers, used by Alembic.
|
|
11
|
+
revision = "3d7f57a61d67"
|
|
12
|
+
down_revision = "4e57407b8e4a"
|
|
13
|
+
branch_labels = ()
|
|
14
|
+
depends_on = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def upgrade():
|
|
18
|
+
"""Upgrade database."""
|
|
19
|
+
op.alter_column(
|
|
20
|
+
"oauth2server_token",
|
|
21
|
+
"expires",
|
|
22
|
+
type_=db.UTCDateTime(),
|
|
23
|
+
existing_type=db.DateTime(),
|
|
24
|
+
existing_nullable=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def downgrade():
|
|
29
|
+
"""Downgrade database."""
|
|
30
|
+
op.alter_column(
|
|
31
|
+
"oauth2server_token",
|
|
32
|
+
"expires",
|
|
33
|
+
type_=db.DateTime(),
|
|
34
|
+
existing_type=db.UTCDateTime(),
|
|
35
|
+
existing_nullable=True,
|
|
36
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2017-2018 CERN.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Add on delete cascade."""
|
|
5
|
+
|
|
6
|
+
from alembic import op
|
|
7
|
+
|
|
8
|
+
# revision identifiers, used by Alembic.
|
|
9
|
+
revision = "4e57407b8e4a"
|
|
10
|
+
down_revision = "12a88921ada2"
|
|
11
|
+
branch_labels = ()
|
|
12
|
+
depends_on = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def upgrade():
|
|
16
|
+
"""Upgrade database."""
|
|
17
|
+
op.drop_constraint(
|
|
18
|
+
"fk_oauth2server_client_user_id_accounts_user",
|
|
19
|
+
"oauth2server_client",
|
|
20
|
+
type_="foreignkey",
|
|
21
|
+
)
|
|
22
|
+
op.create_foreign_key(
|
|
23
|
+
op.f("fk_oauth2server_client_user_id_accounts_user"),
|
|
24
|
+
"oauth2server_client",
|
|
25
|
+
"accounts_user",
|
|
26
|
+
["user_id"],
|
|
27
|
+
["id"],
|
|
28
|
+
ondelete="CASCADE",
|
|
29
|
+
)
|
|
30
|
+
op.create_index(
|
|
31
|
+
op.f("ix_oauth2server_client_user_id"),
|
|
32
|
+
"oauth2server_client",
|
|
33
|
+
["user_id"],
|
|
34
|
+
unique=False,
|
|
35
|
+
)
|
|
36
|
+
op.drop_constraint(
|
|
37
|
+
"fk_oauth2server_token_user_id_accounts_user",
|
|
38
|
+
"oauth2server_token",
|
|
39
|
+
type_="foreignkey",
|
|
40
|
+
)
|
|
41
|
+
op.create_foreign_key(
|
|
42
|
+
op.f("fk_oauth2server_token_user_id_accounts_user"),
|
|
43
|
+
"oauth2server_token",
|
|
44
|
+
"accounts_user",
|
|
45
|
+
["user_id"],
|
|
46
|
+
["id"],
|
|
47
|
+
ondelete="CASCADE",
|
|
48
|
+
)
|
|
49
|
+
op.create_index(
|
|
50
|
+
op.f("ix_oauth2server_token_user_id"),
|
|
51
|
+
"oauth2server_token",
|
|
52
|
+
["user_id"],
|
|
53
|
+
unique=False,
|
|
54
|
+
)
|
|
55
|
+
op.drop_constraint(
|
|
56
|
+
"fk_oauth2server_token_client_id_oauth2server_client",
|
|
57
|
+
"oauth2server_token",
|
|
58
|
+
type_="foreignkey",
|
|
59
|
+
)
|
|
60
|
+
op.create_foreign_key(
|
|
61
|
+
op.f("fk_oauth2server_token_client_id_oauth2server_client"),
|
|
62
|
+
"oauth2server_token",
|
|
63
|
+
"oauth2server_client",
|
|
64
|
+
["client_id"],
|
|
65
|
+
["client_id"],
|
|
66
|
+
ondelete="CASCADE",
|
|
67
|
+
)
|
|
68
|
+
op.create_index(
|
|
69
|
+
op.f("ix_oauth2server_token_client_id"),
|
|
70
|
+
"oauth2server_token",
|
|
71
|
+
["client_id"],
|
|
72
|
+
unique=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def downgrade():
|
|
77
|
+
"""Downgrade database."""
|
|
78
|
+
op.drop_constraint(
|
|
79
|
+
op.f("fk_oauth2server_token_user_id_accounts_user"),
|
|
80
|
+
"oauth2server_token",
|
|
81
|
+
type_="foreignkey",
|
|
82
|
+
)
|
|
83
|
+
op.drop_index(
|
|
84
|
+
op.f("ix_oauth2server_token_user_id"), table_name="oauth2server_token"
|
|
85
|
+
)
|
|
86
|
+
op.create_foreign_key(
|
|
87
|
+
"fk_oauth2server_token_user_id_accounts_user",
|
|
88
|
+
"oauth2server_token",
|
|
89
|
+
"accounts_user",
|
|
90
|
+
["user_id"],
|
|
91
|
+
["id"],
|
|
92
|
+
)
|
|
93
|
+
op.drop_constraint(
|
|
94
|
+
op.f("fk_oauth2server_token_client_id_oauth2server_client"),
|
|
95
|
+
"oauth2server_token",
|
|
96
|
+
type_="foreignkey",
|
|
97
|
+
)
|
|
98
|
+
op.drop_index(
|
|
99
|
+
op.f("ix_oauth2server_token_client_id"), table_name="oauth2server_token"
|
|
100
|
+
)
|
|
101
|
+
op.create_foreign_key(
|
|
102
|
+
"fk_oauth2server_token_client_id_oauth2server_client",
|
|
103
|
+
"oauth2server_token",
|
|
104
|
+
"oauth2server_client",
|
|
105
|
+
["client_id"],
|
|
106
|
+
["client_id"],
|
|
107
|
+
)
|
|
108
|
+
op.drop_constraint(
|
|
109
|
+
op.f("fk_oauth2server_client_user_id_accounts_user"),
|
|
110
|
+
"oauth2server_client",
|
|
111
|
+
type_="foreignkey",
|
|
112
|
+
)
|
|
113
|
+
op.drop_index(
|
|
114
|
+
op.f("ix_oauth2server_client_user_id"), table_name="oauth2server_client"
|
|
115
|
+
)
|
|
116
|
+
op.create_foreign_key(
|
|
117
|
+
"fk_oauth2server_client_user_id_accounts_user",
|
|
118
|
+
"oauth2server_client",
|
|
119
|
+
"accounts_user",
|
|
120
|
+
["user_id"],
|
|
121
|
+
["id"],
|
|
122
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2016-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2023 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Create oauth2server branch."""
|
|
6
|
+
|
|
7
|
+
# revision identifiers, used by Alembic.
|
|
8
|
+
revision = "aa546f2a8d2f"
|
|
9
|
+
down_revision = None
|
|
10
|
+
branch_labels = ("invenio_oauth2server",)
|
|
11
|
+
depends_on = "dbdbc1b19cf2"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def upgrade():
|
|
15
|
+
"""Upgrade database."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def downgrade():
|
|
20
|
+
"""Downgrade database."""
|
|
21
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2024 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""CLI commands for managing the OAuth server."""
|
|
6
|
+
|
|
7
|
+
from functools import wraps
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from flask.cli import with_appcontext
|
|
11
|
+
from invenio_accounts.models import User
|
|
12
|
+
from invenio_db import db
|
|
13
|
+
from werkzeug.local import LocalProxy
|
|
14
|
+
|
|
15
|
+
from .models import Client, Token
|
|
16
|
+
from .validators import validate_scopes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def lazy_result(f):
|
|
20
|
+
"""Decorate function to return LazyProxy."""
|
|
21
|
+
|
|
22
|
+
@wraps(f)
|
|
23
|
+
def decorated(ctx, param, value):
|
|
24
|
+
return LocalProxy(lambda: f(ctx, param, value))
|
|
25
|
+
|
|
26
|
+
return decorated
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@lazy_result
|
|
30
|
+
def process_user(ctx, param, value):
|
|
31
|
+
"""Return a user if exists."""
|
|
32
|
+
if value:
|
|
33
|
+
if value.isdigit():
|
|
34
|
+
user = db.session.get(User, str(value))
|
|
35
|
+
else:
|
|
36
|
+
user = User.query.filter(User.email == value).one_or_none()
|
|
37
|
+
return user
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@lazy_result
|
|
41
|
+
def process_scopes(ctx, param, values):
|
|
42
|
+
"""Return a user if exists."""
|
|
43
|
+
validate_scopes(values)
|
|
44
|
+
return values
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@click.group()
|
|
48
|
+
def tokens():
|
|
49
|
+
"""OAuth2 server token commands."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@tokens.command("create")
|
|
53
|
+
@click.option("-n", "--name", required=True)
|
|
54
|
+
@click.option(
|
|
55
|
+
"-u", "--user", required=True, callback=process_user, help="User ID or email."
|
|
56
|
+
)
|
|
57
|
+
@click.option("-s", "--scope", "scopes", multiple=True, callback=process_scopes)
|
|
58
|
+
@click.option("-i", "--internal", is_flag=True)
|
|
59
|
+
@with_appcontext
|
|
60
|
+
def tokens_create(name, user, scopes, internal):
|
|
61
|
+
"""Create a personal OAuth token."""
|
|
62
|
+
token = Token.create_personal(name, user.id, scopes=scopes, is_internal=internal)
|
|
63
|
+
db.session.commit()
|
|
64
|
+
click.secho(token.access_token, fg="blue")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@tokens.command("delete")
|
|
68
|
+
@click.option("-n", "--name")
|
|
69
|
+
@click.option("-u", "--user", callback=process_user, help="User ID or email.")
|
|
70
|
+
@click.option("--token", "read_access_token", is_flag=True)
|
|
71
|
+
@click.option("--force", is_flag=True)
|
|
72
|
+
@with_appcontext
|
|
73
|
+
def tokens_delete(name=None, user=None, read_access_token=None, force=False):
|
|
74
|
+
"""Delete a personal OAuth token."""
|
|
75
|
+
if not (name or user) and not read_access_token:
|
|
76
|
+
click.get_current_context().fail(
|
|
77
|
+
'You have to pass either a "name" and "user" or the "token"'
|
|
78
|
+
)
|
|
79
|
+
if name and user:
|
|
80
|
+
client = Client.query.filter(
|
|
81
|
+
Client.user_id == user.id, Client.name == name, Client.is_internal.is_(True)
|
|
82
|
+
).one()
|
|
83
|
+
token = Token.query.filter(
|
|
84
|
+
Token.user_id == user.id,
|
|
85
|
+
Token.is_personal.is_(True),
|
|
86
|
+
Token.client_id == client.client_id,
|
|
87
|
+
).one()
|
|
88
|
+
elif read_access_token:
|
|
89
|
+
access_token = click.prompt("Token", hide_input=True)
|
|
90
|
+
token = Token.query.filter(Token.access_token == access_token).one()
|
|
91
|
+
else:
|
|
92
|
+
click.get_current_context().fail("No token was found with provided")
|
|
93
|
+
if force or click.confirm("Are you sure you want to delete the token?"):
|
|
94
|
+
db.session.delete(token)
|
|
95
|
+
db.session.commit()
|
|
96
|
+
click.secho('Token "{}" deleted.'.format(token.access_token), fg="yellow")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2015-2018 CERN.
|
|
2
|
+
# SPDX-FileCopyrightText: 2023 Graz University of Technology.
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""OAuth2Server configuration variables."""
|
|
6
|
+
|
|
7
|
+
OAUTH2_CACHE_TYPE = "redis"
|
|
8
|
+
"""Type of cache to use for storing the temporary grant token."""
|
|
9
|
+
|
|
10
|
+
OAUTH2_PROVIDER_ERROR_ENDPOINT = "invenio_oauth2server.errors"
|
|
11
|
+
"""Error view endpoint."""
|
|
12
|
+
|
|
13
|
+
OAUTH2SERVER_CLIENT_ID_SALT_LEN = 40
|
|
14
|
+
"""Length of client id."""
|
|
15
|
+
|
|
16
|
+
OAUTH2SERVER_CLIENT_SECRET_SALT_LEN = 60
|
|
17
|
+
"""Length of the client secret."""
|
|
18
|
+
|
|
19
|
+
OAUTH2SERVER_TOKEN_PERSONAL_SALT_LEN = 60
|
|
20
|
+
"""Length of the personal access token."""
|
|
21
|
+
|
|
22
|
+
OAUTH2SERVER_ALLOWED_GRANT_TYPES = {
|
|
23
|
+
"authorization_code",
|
|
24
|
+
"client_credentials",
|
|
25
|
+
"refresh_token",
|
|
26
|
+
}
|
|
27
|
+
"""A set of allowed grant types.
|
|
28
|
+
|
|
29
|
+
The allowed values are ``authorization_code``, ``password``,
|
|
30
|
+
``client_credentials``, ``refresh_token``). By default password is disabled,
|
|
31
|
+
as it requires the client application to gain access to the username and
|
|
32
|
+
password of the resource owner.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
OAUTH2SERVER_ALLOWED_RESPONSE_TYPES = {
|
|
36
|
+
"code",
|
|
37
|
+
"token",
|
|
38
|
+
}
|
|
39
|
+
"""A set of allowed response types.
|
|
40
|
+
|
|
41
|
+
The allowed values are ``code`` and ``token``.
|
|
42
|
+
|
|
43
|
+
- ``code`` is used for authorization_code grant types
|
|
44
|
+
- ``token`` is used for implicit grant types
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
OAUTH2SERVER_ALLOWED_URLENCODE_CHARACTERS = "=&;:%+~,*@!()/?"
|
|
48
|
+
"""A string of special characters that should be valid inside a query string.
|
|
49
|
+
|
|
50
|
+
.. seealso::
|
|
51
|
+
|
|
52
|
+
See :py:func:`monkeypatch_oauthlib_urlencode_chars
|
|
53
|
+
<invenio_oauth2server.ext.InvenioOAuth2ServerREST.monkeypatch_oauthlib_urlencode_chars>`
|
|
54
|
+
for a full explanation.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
OAUTH2SERVER_JWT_AUTH_HEADER = "Authorization"
|
|
58
|
+
"""Header for the JWT.
|
|
59
|
+
|
|
60
|
+
.. note::
|
|
61
|
+
|
|
62
|
+
Authorization: Bearer xxx
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
OAUTH2SERVER_JWT_AUTH_HEADER_TYPE = "Bearer"
|
|
66
|
+
"""Header Authorization type.
|
|
67
|
+
|
|
68
|
+
.. note::
|
|
69
|
+
|
|
70
|
+
By default the authorization type is ``Bearer`` as recommented by
|
|
71
|
+
`JWT <https://jwt.io>`_
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
OAUTH2SERVER_JWT_VERIFICATION_FACTORY = "invenio_oauth2server.utils:" "jwt_verify_token"
|
|
75
|
+
"""Import path of factory used to verify JWT.
|
|
76
|
+
|
|
77
|
+
The ``request.headers`` should be passed as parameter.
|
|
78
|
+
"""
|