underscorepy 0.9.0__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.
- _/__init__.py +46 -0
- _/application.py +278 -0
- _/auth.py +163 -0
- _/caches/__init__.py +50 -0
- _/caches/dbcache.py +140 -0
- _/caches/memory.py +77 -0
- _/caches/redis.py +104 -0
- _/components.py +107 -0
- _/databases/__init__.py +282 -0
- _/databases/postgres.py +38 -0
- _/databases/sqlite.py +78 -0
- _/handlers.py +45 -0
- _/logins/__init__.py +82 -0
- _/logins/dblogin.py +230 -0
- _/logins/facebook.py +40 -0
- _/logins/gitlab.py +58 -0
- _/logins/google.py +20 -0
- _/logins/htpasswd.py +47 -0
- _/logins/oauth2.py +52 -0
- _/logins/slap.py +56 -0
- _/records/Protobuf.proto +21 -0
- _/records/__init__.py +186 -0
- _/records/data.py +232 -0
- _/records/protobuf.py +496 -0
- _/settings.py +159 -0
- _/supports/__init__.py +28 -0
- _/supports/nginx/__init__.py +78 -0
- _/supports/nginx/nginx.conf +59 -0
- _/supports/systemd/__init__.py +60 -0
- _/supports/systemd/systemd.service +12 -0
- _/test.py +20 -0
- _/utils.py +85 -0
- _/version.py +5 -0
- _/websockets.py +57 -0
- underscorepy-0.9.0.dist-info/METADATA +85 -0
- underscorepy-0.9.0.dist-info/RECORD +39 -0
- underscorepy-0.9.0.dist-info/WHEEL +5 -0
- underscorepy-0.9.0.dist-info/licenses/LICENSE +22 -0
- underscorepy-0.9.0.dist-info/top_level.txt +1 -0
_/handlers.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import tornado.web
|
|
12
|
+
|
|
13
|
+
import _
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Template(tornado.web.RequestHandler):
|
|
17
|
+
def initialize(self, template='index', **kwds):
|
|
18
|
+
self.kwds = kwds
|
|
19
|
+
self.template = template + '.html'
|
|
20
|
+
|
|
21
|
+
def get(self, template=None, *args):
|
|
22
|
+
template = template + '.html' if template else self.template
|
|
23
|
+
# the template name can come from the URL, so a missing one is not found
|
|
24
|
+
if not os.path.exists(os.path.join(self.get_template_path(), template)):
|
|
25
|
+
raise _.HTTPError(404)
|
|
26
|
+
self.render(template, extra=args, **self.kwds)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Protected(Template):
|
|
30
|
+
async def prepare(self):
|
|
31
|
+
if _.sessions is None:
|
|
32
|
+
raise _.HTTPError(500, 'No session component specified')
|
|
33
|
+
|
|
34
|
+
self.session = None
|
|
35
|
+
session_id = self.get_signed_cookie('session_id', max_age_days=1)
|
|
36
|
+
if session_id:
|
|
37
|
+
session_id = session_id.decode('utf-8')
|
|
38
|
+
self.session = await _.wait(_.sessions.load_session(session_id))
|
|
39
|
+
|
|
40
|
+
def get_current_user(self):
|
|
41
|
+
return self.session
|
|
42
|
+
|
|
43
|
+
@_.auth.current_user
|
|
44
|
+
def get(self, template=None, *args):
|
|
45
|
+
super().get(template, *args)
|
_/logins/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
import urllib
|
|
3
|
+
|
|
4
|
+
import tornado.web
|
|
5
|
+
|
|
6
|
+
import _
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Login(tornado.web.RequestHandler):
|
|
10
|
+
@classmethod
|
|
11
|
+
async def _(cls, component_name, **kwds):
|
|
12
|
+
# create a dynamic child class with kwds from the ini file
|
|
13
|
+
# add a reference to the component name accessible by the new type
|
|
14
|
+
members = _.prefix(kwds)
|
|
15
|
+
members['_component'] = component_name
|
|
16
|
+
login_cls = type(cls.__name__, (cls,), members)
|
|
17
|
+
try:
|
|
18
|
+
await login_cls.init(component_name, **kwds)
|
|
19
|
+
except TypeError as e:
|
|
20
|
+
raise _.error('%s', e)
|
|
21
|
+
_.logins[component_name] = login_cls
|
|
22
|
+
_.application._login_handler('login', login_cls)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
async def init(cls, component_name, **kwds):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
async def args(cls, component_name):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
async def check(cls, username, password):
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def initialize(self):
|
|
37
|
+
self.next_url = self.get_argument('next', '/')
|
|
38
|
+
self.redirect_uri = f'{self.request.protocol}://{self.request.host}/login/{self._component}?next={self.next_url}'
|
|
39
|
+
|
|
40
|
+
async def on_login_success(self, user):
|
|
41
|
+
fn = getattr(self.application, f'on_{self._component}_login_success', self.application.on_login_success)
|
|
42
|
+
try:
|
|
43
|
+
session = await _.wait(fn(self, user))
|
|
44
|
+
await _.wait(_.sessions.save_session(session))
|
|
45
|
+
self.set_signed_cookie('session_id', session['session_id'], expires_days=1)
|
|
46
|
+
except NotImplementedError:
|
|
47
|
+
raise _.HTTPError(500, 'on_login_success method not implemented') from None
|
|
48
|
+
self.redirect(self.next_url)
|
|
49
|
+
|
|
50
|
+
async def on_login_failure(self, message='Invalid Login'):
|
|
51
|
+
fn = getattr(self.application, f'on_{self._component}_login_failure', self.application.on_login_failure)
|
|
52
|
+
await _.wait(fn(self._component))
|
|
53
|
+
|
|
54
|
+
url = self.get_login_url()
|
|
55
|
+
url += "?" + urllib.parse.urlencode(dict(message=message, next_url=self.next_url))
|
|
56
|
+
self.clear_cookie('session_id')
|
|
57
|
+
self.redirect(url)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LoginPage(tornado.web.RequestHandler):
|
|
61
|
+
def initialize(self, template='login', **kwds):
|
|
62
|
+
self.kwds = kwds
|
|
63
|
+
self.template = template + '.html'
|
|
64
|
+
|
|
65
|
+
def get(self, template=None):
|
|
66
|
+
template = template + '.html' if template else self.template
|
|
67
|
+
message = self.get_argument('message', None)
|
|
68
|
+
next_url = self.get_argument('next', '/')
|
|
69
|
+
self.render(template, message=message, next_url=next_url, **self.kwds)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Logout(tornado.web.RequestHandler):
|
|
73
|
+
async def get(self):
|
|
74
|
+
session_id = self.get_signed_cookie('session_id', max_age_days=1)
|
|
75
|
+
|
|
76
|
+
if session_id:
|
|
77
|
+
session_id = session_id.decode('utf-8')
|
|
78
|
+
await _.wait(self.application.on_logout(session_id))
|
|
79
|
+
self.session = await _.wait(_.sessions.clear_session(session_id))
|
|
80
|
+
|
|
81
|
+
self.clear_cookie('session_id')
|
|
82
|
+
self.redirect(self.get_argument('next', '/'))
|
_/logins/dblogin.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import typing
|
|
13
|
+
|
|
14
|
+
import sqlalchemy
|
|
15
|
+
|
|
16
|
+
import _
|
|
17
|
+
import _.records
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DbLogin(_.logins.Login):
|
|
21
|
+
_table = 'users'
|
|
22
|
+
_username = 'username'
|
|
23
|
+
_password = 'password'
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
async def init(cls, component_name, database=None, table=None, username=None, password=None, **kwds):
|
|
27
|
+
cls._database = database
|
|
28
|
+
if database is None:
|
|
29
|
+
if 1 == len(_.databases):
|
|
30
|
+
cls._database = list(_.databases.keys())[0]
|
|
31
|
+
else:
|
|
32
|
+
raise _.error('dblogin requires a database to be specified')
|
|
33
|
+
try:
|
|
34
|
+
cls._db = _.databases[cls._database]
|
|
35
|
+
except AttributeError:
|
|
36
|
+
raise _.error('No database specified for %s', component_name)
|
|
37
|
+
|
|
38
|
+
# if only one login specified use short argument
|
|
39
|
+
prefix = f'{component_name}-' if len(_.config['logins']) > 1 else ''
|
|
40
|
+
|
|
41
|
+
_.argparser.add_argument(f'--{prefix}add-user',
|
|
42
|
+
metavar='<arg>', nargs=2,
|
|
43
|
+
help='create or update user with password'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_.argparser.add_argument(f'--{prefix}list-users',
|
|
47
|
+
action='store_true',
|
|
48
|
+
help='list users'
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# create the dblogin table
|
|
52
|
+
annotations = {
|
|
53
|
+
cls._username : typing.Optional[str],
|
|
54
|
+
cls._password : typing.Optional[str],
|
|
55
|
+
}
|
|
56
|
+
columns = {
|
|
57
|
+
'__tablename__' : cls._table,
|
|
58
|
+
'__annotations__' : annotations,
|
|
59
|
+
'__primary_key__' : cls._username,
|
|
60
|
+
cls._username : sqlalchemy.orm.mapped_column(primary_key=True, init=False),
|
|
61
|
+
cls._password : sqlalchemy.orm.mapped_column(init=False),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for col,dbtype in kwds.items():
|
|
65
|
+
if not dbtype:
|
|
66
|
+
dbtype = 'str'
|
|
67
|
+
annotations[col] = typing.Optional[__builtins__.get(dbtype)]
|
|
68
|
+
columns[col] = sqlalchemy.orm.mapped_column(init=False)
|
|
69
|
+
|
|
70
|
+
table_cls = type(cls._table, (_.records.RecordsInterface,cls._db.Base,), columns)
|
|
71
|
+
|
|
72
|
+
cls._table = table_cls
|
|
73
|
+
|
|
74
|
+
members = {
|
|
75
|
+
'component' : component_name,
|
|
76
|
+
'db' : cls._db,
|
|
77
|
+
'table' : cls._table,
|
|
78
|
+
'username' : cls._username,
|
|
79
|
+
'password' : cls._password,
|
|
80
|
+
}
|
|
81
|
+
subclass = type('DBLoginRecords', (DBLoginRecords,), _.prefix(members))
|
|
82
|
+
_.application._record_handler('logins', subclass)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
async def args(cls, component_name):
|
|
86
|
+
# if only one login specified use short argument
|
|
87
|
+
prefix = f'{component_name}_' if len(_.config['logins']) > 1 else ''
|
|
88
|
+
|
|
89
|
+
add_user = getattr(_.args, f'{prefix}add_user')
|
|
90
|
+
if add_user:
|
|
91
|
+
username,password = add_user
|
|
92
|
+
password = _.auth.hash_password(password)
|
|
93
|
+
|
|
94
|
+
# the section is optional, it only adds extra columns
|
|
95
|
+
section = _.config[component_name] if _.config.has_section(component_name) else {}
|
|
96
|
+
record = dict((k,None) for k in section)
|
|
97
|
+
record.pop('database', None)
|
|
98
|
+
record.pop('table', None)
|
|
99
|
+
|
|
100
|
+
record[cls._username] = username
|
|
101
|
+
record[cls._password] = password
|
|
102
|
+
|
|
103
|
+
user = cls._table._from_dict(**record)
|
|
104
|
+
|
|
105
|
+
callback = getattr(_.application, f'on_{component_name}_add_user', None)
|
|
106
|
+
if callback is None:
|
|
107
|
+
callback = getattr(_.application, 'on_dblogin_add_user', None)
|
|
108
|
+
if callback:
|
|
109
|
+
await _.wait(callback(component_name, user))
|
|
110
|
+
|
|
111
|
+
await cls._db.upsert(user)
|
|
112
|
+
_.application.stop()
|
|
113
|
+
|
|
114
|
+
if getattr(_.args, f'{prefix}list_users'):
|
|
115
|
+
for user in await cls._db.find(cls._table):
|
|
116
|
+
user = user._as_dict()
|
|
117
|
+
user.pop(cls._password)
|
|
118
|
+
print(', '.join(f'{k}: {v}' for k,v in user.items()))
|
|
119
|
+
_.application.stop()
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
async def check(cls, username, password):
|
|
123
|
+
try:
|
|
124
|
+
record = await cls._db.find_one(cls._table, username)
|
|
125
|
+
except _.error as e:
|
|
126
|
+
logging.warning('%s', e)
|
|
127
|
+
record = None
|
|
128
|
+
|
|
129
|
+
if record is None:
|
|
130
|
+
logging.warning('No user: %s', username)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
stored = record[cls._password]
|
|
134
|
+
# hashing is CPU bound so keep it off the event loop
|
|
135
|
+
if not await asyncio.to_thread(_.auth.verify_password, password, stored, username):
|
|
136
|
+
logging.warning('Bad password: %s', username)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
# transparently upgrade legacy or weaker hashes
|
|
140
|
+
if _.auth.needs_rehash(stored):
|
|
141
|
+
record[cls._password] = await asyncio.to_thread(_.auth.hash_password, password)
|
|
142
|
+
await cls._db.upsert(record)
|
|
143
|
+
|
|
144
|
+
return record
|
|
145
|
+
|
|
146
|
+
async def post(self):
|
|
147
|
+
username = self.get_argument('username', None)
|
|
148
|
+
password = self.get_argument('password', None)
|
|
149
|
+
|
|
150
|
+
if username is None or password is None:
|
|
151
|
+
raise _.HTTPError(500)
|
|
152
|
+
|
|
153
|
+
user = await self.check(username, password)
|
|
154
|
+
if user:
|
|
155
|
+
await self.on_login_success(user)
|
|
156
|
+
else:
|
|
157
|
+
await self.on_login_failure()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class DBLoginRecords(_.handlers.Protected):
|
|
161
|
+
# READ
|
|
162
|
+
@_.auth.protected
|
|
163
|
+
async def get(self, username=None):
|
|
164
|
+
if username:
|
|
165
|
+
record = await self._db.find_one(self._table, username)
|
|
166
|
+
record = record._as_dict()
|
|
167
|
+
record.pop(self._password, None)
|
|
168
|
+
self.write(record)
|
|
169
|
+
else:
|
|
170
|
+
records = await self._db.find(self._table)
|
|
171
|
+
data = []
|
|
172
|
+
for record in records:
|
|
173
|
+
record = record._as_dict()
|
|
174
|
+
record.pop(self._password, None)
|
|
175
|
+
data.append(record)
|
|
176
|
+
self.write({'data':data})
|
|
177
|
+
|
|
178
|
+
# UPDATE
|
|
179
|
+
@_.auth.protected
|
|
180
|
+
async def put(self, username=None):
|
|
181
|
+
try:
|
|
182
|
+
data = json.loads(self.request.body)
|
|
183
|
+
except json.decoder.JSONDecodeError:
|
|
184
|
+
raise _.HTTPError(500)
|
|
185
|
+
|
|
186
|
+
username = data.get(self._username, None)
|
|
187
|
+
password = data.pop(self._password, None)
|
|
188
|
+
if not username or not password:
|
|
189
|
+
raise _.HTTPError(500)
|
|
190
|
+
|
|
191
|
+
entry = dict(_.config[self._component])
|
|
192
|
+
entry.pop('database', None)
|
|
193
|
+
entry.pop('table', None)
|
|
194
|
+
prune = list(entry.keys()) + [self._username, self._password]
|
|
195
|
+
|
|
196
|
+
for key in list(data.keys()):
|
|
197
|
+
if key not in prune:
|
|
198
|
+
data.pop(key)
|
|
199
|
+
|
|
200
|
+
user = self._table()
|
|
201
|
+
user(**data)
|
|
202
|
+
|
|
203
|
+
callback = getattr(_.application, f'on_{self._component}_add_user', None)
|
|
204
|
+
if callback is None:
|
|
205
|
+
callback = getattr(_.application, 'on_dblogin_add_user', None)
|
|
206
|
+
if callback:
|
|
207
|
+
await _.wait(callback(self._component, user))
|
|
208
|
+
|
|
209
|
+
if not user[self._password]:
|
|
210
|
+
password = await asyncio.to_thread(_.auth.hash_password, password)
|
|
211
|
+
user[self._password] = password
|
|
212
|
+
|
|
213
|
+
await self._db.insert(user)
|
|
214
|
+
self.set_status(204)
|
|
215
|
+
|
|
216
|
+
# DELETE
|
|
217
|
+
@_.auth.protected
|
|
218
|
+
async def delete(self, username=None):
|
|
219
|
+
self.set_status(204)
|
|
220
|
+
if username is None:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
user = await self._db.find_one(self._table, username)
|
|
224
|
+
await self._db.delete(user)
|
|
225
|
+
|
|
226
|
+
callback = getattr(_.application, f'on_{self._component}_delete', None)
|
|
227
|
+
if callback is None:
|
|
228
|
+
callback = getattr(_.application, 'on_dblogin_delete', None)
|
|
229
|
+
if callback:
|
|
230
|
+
await _.wait(callback(self._component, username))
|
_/logins/facebook.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
11
|
+
import _
|
|
12
|
+
|
|
13
|
+
import tornado.auth
|
|
14
|
+
import tornado.web
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Facebook(_.logins.Login, tornado.auth.FacebookGraphMixin):
|
|
18
|
+
@classmethod
|
|
19
|
+
async def init(cls, component_name, **kwds):
|
|
20
|
+
cls.extra = {"scope": "read_stream,offline_access"}
|
|
21
|
+
|
|
22
|
+
async def get(self):
|
|
23
|
+
try:
|
|
24
|
+
code = self.get_argument('code', None)
|
|
25
|
+
if not code:
|
|
26
|
+
self.authorize_redirect(
|
|
27
|
+
redirect_uri = self.redirect_uri,
|
|
28
|
+
client_id = self.client_id,
|
|
29
|
+
extra_params = self.extra
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
user = await self.get_authenticated_user(
|
|
33
|
+
redirect_uri = self.redirect_uri,
|
|
34
|
+
client_id = self.client_id,
|
|
35
|
+
client_secret = self.client_secret,
|
|
36
|
+
code = code,
|
|
37
|
+
)
|
|
38
|
+
await self.on_login_success(user)
|
|
39
|
+
except tornado.httpclient.HTTPClientError as e:
|
|
40
|
+
await self.on_login_failure(str(e))
|
_/logins/gitlab.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
# This is for logging into GitLab servers and serves as an example
|
|
10
|
+
# of implementing an OAuth2 handler for any service that supports it
|
|
11
|
+
|
|
12
|
+
# For admins go to http(s)://your.gitlab.server/admin/applications and click "New application"
|
|
13
|
+
# For users go to http(s)://your.gitlab.server/-/profile/applications
|
|
14
|
+
# For groups go to http(s)://your.gitlab.server/groups/(group)/-/settings/applications
|
|
15
|
+
#
|
|
16
|
+
# Pick a "Name" for the application
|
|
17
|
+
#
|
|
18
|
+
# Set the "Redirect URI" to http(s)://your.underscore.app/login/gitlab
|
|
19
|
+
# (or replace gitlab at the end with whatever unique name assigned in the ini file)
|
|
20
|
+
#
|
|
21
|
+
# Select Trusted and Confidential
|
|
22
|
+
#
|
|
23
|
+
# "read_user" is the only scope required by this login plugin
|
|
24
|
+
#
|
|
25
|
+
# Click "Save application"
|
|
26
|
+
#
|
|
27
|
+
# Copy "Application ID" as client_id and copy "Secret" as client_secret in the ini
|
|
28
|
+
#
|
|
29
|
+
# Click "Continue"
|
|
30
|
+
#
|
|
31
|
+
# Your underscore app can now authenticate via your GitLab server
|
|
32
|
+
#
|
|
33
|
+
|
|
34
|
+
# [logins]
|
|
35
|
+
# gitlab
|
|
36
|
+
#
|
|
37
|
+
# [gitlab]
|
|
38
|
+
# gitlab = https://your.gitlab.server
|
|
39
|
+
# client_id = <client_id>
|
|
40
|
+
# client_secret = <client_secret>
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
import _
|
|
44
|
+
|
|
45
|
+
from .oauth2 import OAuth2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class GitLab(OAuth2, _.logins.Login):
|
|
49
|
+
@classmethod
|
|
50
|
+
async def init(cls, component_name, gitlab, **kwds):
|
|
51
|
+
cls.scope = ['read_user']
|
|
52
|
+
|
|
53
|
+
cls._OAUTH_AUTHORIZE_URL = f'{gitlab}/oauth/authorize'
|
|
54
|
+
cls._OAUTH_ACCESS_TOKEN_URL = f'{gitlab}/oauth/token'
|
|
55
|
+
cls._OAUTH_USERINFO_URL = f'{gitlab}/api/v4/user'
|
|
56
|
+
cls._OAUTH_SETTINGS_KEY = f'{component_name}_oauth'
|
|
57
|
+
|
|
58
|
+
await super().init(component_name, **kwds)
|
_/logins/google.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import _
|
|
10
|
+
|
|
11
|
+
from .oauth2 import OAuth2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Google(OAuth2, _.logins.Login):
|
|
15
|
+
@classmethod
|
|
16
|
+
async def init(cls, component_name, **kwds):
|
|
17
|
+
cls.scope = ['email']
|
|
18
|
+
cls.extra = {'approval_prompt': 'auto'}
|
|
19
|
+
|
|
20
|
+
await super().init(component_name, **kwds)
|
_/logins/htpasswd.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import hashlib
|
|
11
|
+
import base64
|
|
12
|
+
|
|
13
|
+
import _
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# this is opening the file and scanning it for every login, which is fine
|
|
17
|
+
# for small projects with a limited number of users.
|
|
18
|
+
#
|
|
19
|
+
# Passwords can be managed with Apache's htpasswd program
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Htpasswd(_.logins.Login):
|
|
23
|
+
async def post(self):
|
|
24
|
+
username = self.get_argument('username', '')
|
|
25
|
+
password = self.get_argument('password', '')
|
|
26
|
+
password = password.encode('utf-8')
|
|
27
|
+
password = b'{SHA}' + base64.b64encode(hashlib.sha1(password).digest())
|
|
28
|
+
password = password.decode('ascii')
|
|
29
|
+
|
|
30
|
+
path = _.paths(_.config.get('simple', 'path'))
|
|
31
|
+
|
|
32
|
+
fp = open(path, 'r')
|
|
33
|
+
for line in fp:
|
|
34
|
+
if not line:
|
|
35
|
+
continue
|
|
36
|
+
entry,hash = line.split(':', 1)
|
|
37
|
+
|
|
38
|
+
if entry != username:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if hash.rstrip() != password:
|
|
42
|
+
break
|
|
43
|
+
|
|
44
|
+
await self.on_login_success(user)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
await self.on_login_failure()
|
_/logins/oauth2.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import tornado.auth
|
|
10
|
+
import tornado.httpclient
|
|
11
|
+
|
|
12
|
+
import _
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# for Google and Gitlab the GoogleOAuth2Mixin is a good starting point
|
|
16
|
+
class OAuth2(tornado.auth.GoogleOAuth2Mixin):
|
|
17
|
+
extra = None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
async def init(cls, component_name, client_id, client_secret, **kwds):
|
|
21
|
+
_.application.settings[cls._OAUTH_SETTINGS_KEY] = {
|
|
22
|
+
'key' : client_id,
|
|
23
|
+
'secret' : client_secret,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async def get(self):
|
|
27
|
+
code = self.get_argument('code', None)
|
|
28
|
+
if not code:
|
|
29
|
+
# make the initial request to the OAuth2 service
|
|
30
|
+
self.authorize_redirect(
|
|
31
|
+
redirect_uri = self.redirect_uri,
|
|
32
|
+
client_id = self._client_id,
|
|
33
|
+
scope = self.scope,
|
|
34
|
+
response_type = 'code',
|
|
35
|
+
extra_params = self.extra
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
try:
|
|
39
|
+
# the OAuth2 service will callback with a code
|
|
40
|
+
# pass it to the get_authenticated_user function defined
|
|
41
|
+
# in GitLabAuthMixin
|
|
42
|
+
self.oauth = await self.get_authenticated_user(
|
|
43
|
+
redirect_uri = self.redirect_uri,
|
|
44
|
+
code = code,
|
|
45
|
+
)
|
|
46
|
+
user = await self.oauth2_request(
|
|
47
|
+
self._OAUTH_USERINFO_URL,
|
|
48
|
+
access_token = self.oauth["access_token"]
|
|
49
|
+
)
|
|
50
|
+
await self.on_login_success(user)
|
|
51
|
+
except tornado.httpclient.HTTPClientError as e:
|
|
52
|
+
await self.on_login_failure(str(e))
|
_/logins/slap.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
import tornado.escape
|
|
13
|
+
|
|
14
|
+
import _
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import ldap
|
|
19
|
+
except ImportError:
|
|
20
|
+
raise _.error('Missing LDAP module')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Slap(_.logins.Login):
|
|
24
|
+
@classmethod
|
|
25
|
+
async def init(cls, component_name, **kwds):
|
|
26
|
+
_.argparser.add_argument(f'--{component_name}-list-users',
|
|
27
|
+
action='store_true',
|
|
28
|
+
help='list users'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
async def check(cls, username, password):
|
|
33
|
+
try:
|
|
34
|
+
dn = cls.dn.format(username)
|
|
35
|
+
ldap_server = ldap.initialize(cls.uri)
|
|
36
|
+
ldap_server.bind_s(dn, password)
|
|
37
|
+
ldap_server.unbind()
|
|
38
|
+
return True
|
|
39
|
+
except ldap.NO_SUCH_OBJECT:
|
|
40
|
+
logging.warn('Could not find record for user: %s', username)
|
|
41
|
+
except ldap.INVALID_CREDENTIALS:
|
|
42
|
+
logging.warn('Invalid credentials for user: %s', username)
|
|
43
|
+
except ldap.SERVER_DOWN:
|
|
44
|
+
logging.error('Could not connect to LDAP server: %s', cls.uri)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
async def post(self):
|
|
48
|
+
username = self.get_argument('username', '')
|
|
49
|
+
username = tornado.escape.xhtml_escape(username)
|
|
50
|
+
password = self.get_argument('password', '')
|
|
51
|
+
|
|
52
|
+
ok = await self.check(username, password)
|
|
53
|
+
if ok:
|
|
54
|
+
await self.on_login_success({'username':username})
|
|
55
|
+
else:
|
|
56
|
+
await self.on_login_failure()
|
_/records/Protobuf.proto
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
import "google/protobuf/descriptor.proto";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
extend google.protobuf.MessageOptions {
|
|
7
|
+
string id = 50001;
|
|
8
|
+
bool no_table = 50002;
|
|
9
|
+
bool no_handler = 50003;
|
|
10
|
+
string refs = 50004;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
extend google.protobuf.FieldOptions {
|
|
14
|
+
bool pkey = 50001;
|
|
15
|
+
bool no_column = 50002;
|
|
16
|
+
bool uniq = 50003;
|
|
17
|
+
bool required = 50004;
|
|
18
|
+
string ref = 50005;
|
|
19
|
+
string paired = 50006;
|
|
20
|
+
string subtype = 50007;
|
|
21
|
+
}
|