brain2-oc 2.4.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.
brain/__init__.py ADDED
File without changes
brain/__main__.py ADDED
@@ -0,0 +1,56 @@
1
+ # coding=utf8
2
+ """ Brain
3
+
4
+ Handles authorization / user requests
5
+ """
6
+
7
+ __author__ = "Chris Nasr"
8
+ __version__ = "1.0.0"
9
+ __copyright__ = "Ouroboros Coding Inc."
10
+ __email__ = "chris@ouroboroscoding.com"
11
+ __created__ = "2022-08-25"
12
+
13
+ # Python imports
14
+ from sys import argv, exit, stderr
15
+
16
+ def cli():
17
+ """CLI
18
+
19
+ Called from the command line to run from the current directory
20
+
21
+ Returns:
22
+ uint
23
+ """
24
+
25
+ # If we have no arguments
26
+ if len(argv) == 1:
27
+
28
+ # Run the REST server
29
+ from brain import rest
30
+ return rest.run()
31
+
32
+ # Else, if we have one argument
33
+ elif len(argv) == 2:
34
+
35
+ # If we are installing
36
+ if argv[1] == 'install':
37
+ from brain import install
38
+ return install.run()
39
+
40
+ # Else, if we are explicitly stating the rest service
41
+ elif argv[1] == 'rest':
42
+ from brain import rest
43
+ return rest.run()
44
+
45
+ # Else, if we are upgrading
46
+ elif argv[1] == 'upgrade':
47
+ from brain import upgrade
48
+ return upgrade.run()
49
+
50
+ # Else, arguments are wrong, print and return an error
51
+ print('Invalid arguments', file = stderr)
52
+ return 1
53
+
54
+ # Only run if called directly
55
+ if __name__ == '__main__':
56
+ exit(cli())
brain/define/.git ADDED
@@ -0,0 +1 @@
1
+ gitdir: ../../.git/modules/brain/define
brain/define/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # brain2-define
2
+ Define files for the Brain2 service
@@ -0,0 +1,8 @@
1
+ {
2
+ "SIGNIN_FAILED": 1200,
3
+ "PASSWORD_STRENGTH": 1201,
4
+ "BAD_PORTAL": 1202,
5
+ "INTERNAL_KEY": 1203,
6
+ "BAD_OAUTH": 1204,
7
+ "BAD_CONFIG": 1205
8
+ }
brain/define/key.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "_id": {
3
+ "__type__": "string",
4
+ "__regex__": "^[0-9a-f]{32}$",
5
+ "__minimum__": 32,
6
+ "__maximum__": 32
7
+ },
8
+
9
+ "_created": {
10
+ "__type__": "timestamp",
11
+ "__optional__": true
12
+ },
13
+
14
+ "_updated": {
15
+ "__type__": "timestamp",
16
+ "__optional__": true
17
+ },
18
+
19
+ "user": {
20
+ "__type__": "tuuid"
21
+ },
22
+
23
+ "type": {
24
+ "__type__": "string",
25
+ "__options__": [ "forgot", "setup", "verify" ]
26
+ }
27
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "_user": {
3
+ "__type__": "tuuid"
4
+ },
5
+
6
+ "_portal": {
7
+ "__type__": "string",
8
+ "__maximum__": 16
9
+ },
10
+
11
+ "name": {
12
+ "__type__": "string",
13
+ "__regex__": "[a-z_]{1,32}"
14
+ },
15
+
16
+ "id": {
17
+ "__type__": "tuuid"
18
+ },
19
+
20
+ "rights": {
21
+ "__type__": "uint",
22
+ "__minimum__": 1,
23
+ "__maximum__": 15
24
+ }
25
+ }
brain/define/user.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "_id": {
3
+ "__type__": "tuuid",
4
+ "__optional__": true
5
+ },
6
+
7
+ "_created": {
8
+ "__type__": "timestamp",
9
+ "__optional__": true
10
+ },
11
+
12
+ "_updated": {
13
+ "__type__": "timestamp",
14
+ "__optional__": true
15
+ },
16
+
17
+ "email": {
18
+ "__type__": "string",
19
+ "__maximum__": 127
20
+ },
21
+
22
+ "passwd": {
23
+ "__type__":"string",
24
+ "__regex__":"^[0-9a-fA-F]{72}$"
25
+ },
26
+
27
+ "locale": {
28
+ "__type__": "string",
29
+ "__regex__": "^[a-z]{2}-[A-Z]{2}$"
30
+ },
31
+
32
+ "first_name": {
33
+ "__type__": "string",
34
+ "__minumum__": 1,
35
+ "__maximum__": 31
36
+ },
37
+
38
+ "last_name": {
39
+ "__type__": "string",
40
+ "__minumum__": 2,
41
+ "__maximum__": 31
42
+ },
43
+
44
+ "title": {
45
+ "__type__": "string",
46
+ "__maximum__": 31,
47
+ "__optional__": true
48
+ },
49
+
50
+ "suffix": {
51
+ "__type__": "string",
52
+ "__maximum__": 31,
53
+ "__optional__": true
54
+ },
55
+
56
+ "phone_number": {
57
+ "__type__": "string",
58
+ "__regex__": "^(\\+[\\d \\(\\)-]{10,30}|[\\d \\(\\)-]{10,31})$",
59
+ "__optional__": true
60
+ },
61
+
62
+ "phone_ext": {
63
+ "__type__": "string",
64
+ "__maximum__": 11,
65
+ "__optional__": true
66
+ },
67
+
68
+ "verified": {
69
+ "__type__": "bool",
70
+ "__optional__": true
71
+ }
72
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": {
3
+ "__type__": "string"
4
+ },
5
+ "right": [ {
6
+ "__type__": "uint",
7
+ "__options__": [ 1, 2, 4, 8 ]
8
+ }, {
9
+ "__array__": "unique",
10
+ "__type__": "uint",
11
+ "__options__": [ 1, 2, 4, 8 ]
12
+ } ],
13
+ "id": [ {
14
+ "__type__": "string",
15
+ "__optional__": true
16
+ }, {
17
+ "__array__": "unique",
18
+ "__type__": "string",
19
+ "__optional__": true
20
+ } ]
21
+ }
brain/errors.py ADDED
@@ -0,0 +1,35 @@
1
+ # coding=utf8
2
+ """ Errors
3
+
4
+ Brain error codes
5
+ """
6
+
7
+ __author__ = "Chris Nasr"
8
+ __copyright__ = "Ouroboros Coding Inc"
9
+ __version__ = "1.0.0"
10
+ __email__ = "chris@ouroboroscoding.com"
11
+ __created__ = "2023-01-16"
12
+
13
+ # Import all body errors as local errors
14
+ from body.errors import *
15
+
16
+ SIGNIN_FAILED = 1200
17
+ """Sign In Failed"""
18
+
19
+ PASSWORD_STRENGTH = 1201
20
+ """Password not strong enough"""
21
+
22
+ BAD_PORTAL = 1202
23
+ """Portal doesn't exist, or the user doesn't have permissions for it"""
24
+
25
+ INTERNAL_KEY = 1203
26
+ """Internal key failed to validate"""
27
+
28
+ BAD_OAUTH = 1204
29
+ """Something failed in the OAuth process"""
30
+
31
+ BAD_CONFIG = 1205
32
+ """Something is missing from the configuration"""
33
+
34
+ __all__ = [ n for n,v in globals().items() if isinstance(v, int) ]
35
+ """ Export all the constants"""
File without changes
@@ -0,0 +1,275 @@
1
+ # coding=utf8
2
+ """ Access
3
+
4
+ Shared methods for verifying access
5
+ """
6
+
7
+ __author__ = "Chris Nasr"
8
+ __copyright__ = "Ouroboros Coding Inc"
9
+ __version__ = "1.0.0"
10
+ __email__ = "chris@ouroboroscoding.com"
11
+ __created__ = "2022-08-29"
12
+
13
+ # Limit exports
14
+ __all__ = [
15
+ 'generate_key', 'internal', 'internal_or_verify', 'SYSTEM_USER_ID' 'verify'
16
+ ]
17
+
18
+ # Ouroboros imports
19
+ import body
20
+ from config import config
21
+ import jobject
22
+ from memory import _Memory
23
+ from nredis import nr
24
+ from strings import random
25
+
26
+ # Python imports
27
+ from hashlib import sha1
28
+ from time import time
29
+ from typing import List, MutableMapping
30
+ from sys import stderr
31
+
32
+ # Package imports
33
+ from brain import errors, rights
34
+
35
+ # Constants
36
+ INTERNAL = 1
37
+ VERIFY = 2
38
+
39
+ ALL = rights.ALL
40
+ A = ALL
41
+ """Allowed to CRUD"""
42
+
43
+ CREATE = rights.CREATE
44
+ C = CREATE
45
+ """Allowed to create records"""
46
+
47
+ DELETE = rights.DELETE
48
+ D = DELETE
49
+ """Allowed to delete records"""
50
+
51
+ READ = rights.READ
52
+ R = READ
53
+ """Allowed to read records"""
54
+
55
+ UPDATE = rights.UPDATE
56
+ U = UPDATE
57
+ """Allowed to update records"""
58
+
59
+ SYSTEM_USER_ID = '00000000000000000000000000000000'
60
+ """System User ID"""
61
+
62
+ RIGHTS_ALL_ID = '012345679abc4defa0123456789abcde'
63
+ """Used to represent rights across the entire system"""
64
+
65
+ _internal = config.brain.internal({
66
+ 'redis': 'session',
67
+ 'salt': '',
68
+ 'ttl': 5
69
+ })
70
+ """Internal Configuration"""
71
+
72
+ _redis_conn = nr(_internal['redis'])
73
+ """Redis Connection"""
74
+
75
+ # Encode the salt to utf-8
76
+ _internal['salt'] = _internal['salt'].encode('utf-8')
77
+
78
+ # Don't allow 0 for ttl, and give a warning if anyone tries
79
+ if _internal['ttl'] <= 0:
80
+ print(
81
+ 'brain.internal.ttl can NOT be less than 0 (zero). Setting to 5 ' \
82
+ '(seconds)',
83
+ file=stderr
84
+ )
85
+ _internal['ttl'] = 5
86
+
87
+ def generate_key(req: MutableMapping) -> MutableMapping:
88
+ """Generate Key
89
+
90
+ Used as a wrapper to generates a key and add it to the passed req which it
91
+ then returns.
92
+
93
+ Arguments:
94
+ req (MutableMapping): The dict or jobject being sent with the request.
95
+
96
+ Returns:
97
+ The same req object passed to it
98
+ """
99
+
100
+ # Pull in the global internal config
101
+ global _internal
102
+
103
+ # Generate a unique ID
104
+ sID = str(random(32, [ 'aZ', '10', '!*' ]))
105
+
106
+ # Get the current timestamp as a string
107
+ sTime = str(int(time()))
108
+
109
+ # Generate a sha1 from the salt and parts of the time
110
+ sSHA1 = sha1(
111
+ sTime[3:].encode('utf-8') +
112
+ _internal['salt'] +
113
+ sTime[:3].encode('utf-8')
114
+ ).hexdigest()
115
+
116
+ # Store the key
117
+ if not _redis_conn.set(name = sID, value = sSHA1, ex = _internal['ttl']):
118
+ return False
119
+
120
+ # Add the meta
121
+ try:
122
+ req['meta']['Authorize-Internal'] = '%s~%s' % ( sID, sTime )
123
+ except KeyError:
124
+ req['meta'] = { 'Authorize-Internal': '%s~%s' % ( sID, sTime ) }
125
+
126
+ # Return the req
127
+ return req
128
+
129
+ def internal(req: jobject):
130
+ """Internal
131
+
132
+ Checks for an internal key and throws an exception if it's missing or
133
+ invalid
134
+
135
+ Arguments:
136
+ req (jobject): The req object passed to the request
137
+
138
+ Raises:
139
+ ResponseException
140
+
141
+ Returns:
142
+ None
143
+ """
144
+
145
+ # Pull in the global internal config
146
+ global _internal
147
+
148
+ # Get the current time stamp as soon as possible
149
+ iNow = int(time())
150
+
151
+ # Use bottle from inside body.rest to get the text ID and time using the
152
+ # Authorize-Internal header
153
+ try:
154
+ sID, sTime = req.meta['Authorize-Internal'].split('~')
155
+ except KeyError:
156
+ raise body.ResponseException(error = (
157
+ errors.INTERNAL_KEY, 'missing'
158
+ ))
159
+
160
+ # Fetch the key from redis
161
+ sKey = _redis_conn.get(sID).decode()
162
+ if sKey is None:
163
+ raise body.ResponseException(error = (
164
+ errors.INTERNAL_KEY, 'no key'
165
+ ))
166
+
167
+ # If the time is not close enough, the rest is irrelevant
168
+ if int(sTime) - iNow > _internal['ttl']:
169
+
170
+ # Raise an exception
171
+ raise body.ResponseException(error = (
172
+ errors.INTERNAL_KEY, 'expired'
173
+ ))
174
+
175
+ # Generate a sha1 from the salt, parts of the text, and the time
176
+ sSHA1 = sha1(
177
+ sTime[3:].encode('utf-8') +
178
+ _internal['salt'] +
179
+ sTime[:3].encode('utf-8')
180
+ ).hexdigest()
181
+
182
+ # If they aren't equal
183
+ if sSHA1 != sKey:
184
+ raise body.ResponseException(error = (
185
+ errors.INTERNAL_KEY, 'invalid'
186
+ ))
187
+
188
+ # Delete the key
189
+ _redis_conn.delete(sID)
190
+
191
+ # Return OK
192
+ return True
193
+
194
+ def internal_or_verify(
195
+ req: jobject,
196
+ permission: dict | List[dict]
197
+ ) -> str:
198
+ """ Internal or Verify
199
+
200
+ Checks for an internal key, if it wasn't sent, does a verify check.
201
+
202
+ Returns the UUID of the user requesting access via the session, else the \
203
+ SYSTEM_USER_ID when the request is made internally
204
+
205
+ Arguments:
206
+ req (jobject): The current request object sent to the request
207
+ permission (dict | dict[]): A dict with 'name', 'right' and optional \
208
+ 'id', or a list of those dicts
209
+
210
+ Raises:
211
+ ResponseException
212
+
213
+ Returns:
214
+ string
215
+ """
216
+
217
+ # If we have an internal key
218
+ if 'meta' in req and 'Authorize-Internal' in req.meta:
219
+
220
+ # Run the internal check
221
+ internal(req)
222
+
223
+ # Return that the request passed the internal check
224
+ return SYSTEM_USER_ID
225
+
226
+ # Else
227
+ else:
228
+
229
+ # Make sure the user has the proper permission to do this
230
+ verify(req.session, permission)
231
+
232
+ # Return that the request passed the verify check
233
+ return req.session.user._id
234
+
235
+ def verify(
236
+ session: _Memory,
237
+ permission: dict | List[dict],
238
+ _return: bool = False
239
+ ) -> True:
240
+ """Verify
241
+
242
+ Checks's if the currently signed in user has the requested right on the
243
+ given permission. If the user has rights, True is returned, else an
244
+ exception of ResponseException is raised
245
+
246
+ Arguments:
247
+ session (memory._Memory): The current session
248
+ permission (dict | dict[]): A dict with 'name', 'right' and optional \
249
+ 'id', or a list of those dicts
250
+ _return (bool): Optional, set to True to return instead of raising
251
+
252
+ Raises:
253
+ ResponseException
254
+
255
+ Returns:
256
+ bool
257
+ """
258
+
259
+ # Check with the authorization service
260
+ oResponse = body.read('brain', 'verify', {
261
+ 'data': permission,
262
+ 'session': session
263
+ })
264
+
265
+ # If the response failed
266
+ if oResponse.error:
267
+ raise body.ResponseException(oResponse)
268
+
269
+ # If the check failed, raise an exception
270
+ if not oResponse.data:
271
+ if _return: return False
272
+ raise body.ResponseException(error = body.errors.RIGHTS)
273
+
274
+ # Return OK
275
+ return True
brain/helpers/users.py ADDED
@@ -0,0 +1,171 @@
1
+ # coding=utf8
2
+ """ Users
3
+
4
+ Shared methods for accessing user info
5
+ """
6
+
7
+ __author__ = "Chris Nasr"
8
+ __copyright__ = "Ouroboros Coding Inc"
9
+ __version__ = "1.0.0"
10
+ __email__ = "chris@ouroboroscoding.com"
11
+ __created__ = "2022-08-29"
12
+
13
+ # Limit exports
14
+ __all__ = [ 'details', 'EMPTY_PASS', 'exists', 'permissions', 'SYSTEM_USER_ID' ]
15
+
16
+ # Ouroboros modules
17
+ from body import read, ResponseException
18
+ import undefined
19
+
20
+ # Python imports
21
+ from typing import List, Literal
22
+
23
+ # Pip imports
24
+ from brain.helpers.access import generate_key, SYSTEM_USER_ID
25
+
26
+ EMPTY_PASS = '000000000000000000000000000000000000' \
27
+ '000000000000000000000000000000000000'
28
+ """Default password value"""
29
+
30
+ def details(
31
+ _id: str | List[str],
32
+ fields: List[str] = undefined,
33
+ order: List[str] = undefined,
34
+ as_dict: Literal[False] | str = '_id'
35
+ ) -> dict | list:
36
+ """Details
37
+
38
+ Fetches user info from IDs and returns a single object if a single ID is \
39
+ passed, else returns a dict with each key representing the ID, with the \
40
+ value being the rest of the user details requested via fields. Setting \
41
+ `as_dict` to False, allows for returning a normal list which will be \
42
+ sorted by `order`
43
+
44
+ Arguments:
45
+ _id (str|str[]) The ID(s) to fetch info for
46
+ fields (str[]): The list of fields to return
47
+ order (str[]): The list of fields to order by
48
+ as_dict (False | str): Optional, if false, returns a list, if set, must
49
+ be a field that's passed
50
+
51
+ Returns:
52
+ dict | list
53
+ """
54
+
55
+ # Init the data by adding the ID(s)
56
+ dData = { '_id': _id }
57
+
58
+ # If we want specific fields
59
+ if fields:
60
+ dData['fields'] = fields
61
+
62
+ # If we want a specific order
63
+ if order:
64
+ dData['order'] = order
65
+
66
+ # Make the read using an internal key
67
+ oResponse = read('brain', 'users/by/id', generate_key({
68
+ 'data': dData
69
+ }))
70
+
71
+ # If there's an error
72
+ if oResponse.error:
73
+
74
+ # Raise it
75
+ raise ResponseException(oResponse)
76
+
77
+ # If we got a single dictionary, or want the original unaltered list
78
+ if not as_dict or isinstance(oResponse.data, dict):
79
+ return oResponse.data
80
+
81
+ # Convert the data into a dictionary
82
+ dUsers = {}
83
+ for d in oResponse.data:
84
+
85
+ # Pop off the field used as a key
86
+ sKey = d.pop(as_dict)
87
+
88
+ # Store the rest by the key
89
+ dUsers[sKey] = d
90
+
91
+ # Return the users
92
+ return dUsers
93
+
94
+ def exists(
95
+ _id: str | List[str]
96
+ ) -> bool:
97
+ """Exists
98
+
99
+ Returns true if all User IDs passed exist in the system
100
+
101
+ Arguments:
102
+ _id (str | str[]): One or more IDs to check
103
+
104
+ Returns:
105
+ bool
106
+ """
107
+
108
+ # Init the data by adding the ID(s)
109
+ dData = { '_id': _id, 'fields': ['_id'] }
110
+
111
+ # Make the read using an internal key
112
+ oResponse = read('brain', 'users/by/id', generate_key({
113
+ 'data': dData
114
+ }))
115
+
116
+ # If there's an error
117
+ if oResponse.error:
118
+
119
+ # Throw it
120
+ raise ResponseException(oResponse)
121
+
122
+ # If we got a string
123
+ if isinstance(_id, str):
124
+
125
+ # Set the return based on whether we got anything or not
126
+ bRet = oResponse.data and True or False
127
+
128
+ # Else, we got a list
129
+ else:
130
+
131
+ # Set the return based on if the counts match
132
+ bRet = len(_id) == len(oResponse.data)
133
+
134
+ # Return
135
+ return bRet
136
+
137
+ def permissions(
138
+ user: str,
139
+ portal: str = undefined
140
+ ) -> dict:
141
+ """Permissions
142
+
143
+ Returns the list of all permissions for the user by portal, or only the
144
+ permissions for one specific portal
145
+
146
+ Arguments:
147
+ user (str): The ID of the user to fetch permissions for
148
+ portal (str): Optional, the specific set of permissions to return
149
+
150
+ Returns:
151
+ dict | None
152
+ """
153
+
154
+ # Init the data by adding the ID
155
+ dData = { 'user': user }
156
+
157
+ # If we have a portal
158
+ if portal is not undefined:
159
+ dData['portal'] = portal
160
+
161
+ # Make the read using an internal key
162
+ oResponse = read('brain', 'permissions', generate_key({
163
+ 'data': dData
164
+ }))
165
+
166
+ # If there's an error, raise it
167
+ if oResponse.error:
168
+ raise ResponseException(oResponse)
169
+
170
+ # Return the permissions
171
+ return oResponse.data