Skip to content

Commit 6cc790c

Browse files
committed
add api for vuser and more
add cli with expire param add test with vuser improve tag gen userctl logger prefer debug fix list root path lower api default storage size
1 parent 7642ef4 commit 6cc790c

File tree

6 files changed

+126
-17
lines changed

6 files changed

+126
-17
lines changed

lfss/api/connector.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ def list_path(
379379
if path == '/':
380380
# handle root path separately
381381
my_username = self.whoami().username
382-
dirnames = [f'{my_username}/'] if not my_username.startswith('.') else [] + [f'{p.username}/' for p in self.peers(AccessLevel.READ)]
382+
dirnames = ([f'{my_username}/'] if not my_username.startswith('.') else []) + [f'{p.username}/' for p in self.peers(AccessLevel.READ)]
383383
return PathContents(
384384
dirs = [DirectoryRecord(url = d) for d in dirnames],
385385
files = []
@@ -537,7 +537,7 @@ def add_user(
537537
username: str,
538538
password: Optional[str] = None,
539539
admin: bool = False,
540-
max_storage: int | str = '100G',
540+
max_storage: int | str = '10G',
541541
permission: FileReadPermission | str = 'unset'
542542
) -> UserRecord:
543543
""" Admin API: Add a new user to the system. """
@@ -552,6 +552,30 @@ def add_user(
552552
response = self._fetch_factory('POST', '_api/user/add', search_params=data)()
553553
return UserRecord(**response.json())
554554

555+
def add_virtual_user(
556+
self,
557+
tag: str = "",
558+
peers: dict[AccessLevel, list[str]] | str = {},
559+
max_storage: int | str = '1G',
560+
expire: Optional[int | str] = None,
561+
) -> UserRecord:
562+
""" Admin API: Add a new virtual (hidden) user to the system. """
563+
data = {
564+
'tag': tag,
565+
'max_storage': str(max_storage),
566+
}
567+
if isinstance(peers, dict):
568+
peer_strs = []
569+
for level, users in peers.items():
570+
peer_strs.append(f"{level.name}:{','.join(users)}")
571+
data['peers'] = ';'.join(peer_strs)
572+
else:
573+
data['peers'] = peers
574+
if expire is not None:
575+
data['expire'] = str(expire)
576+
response = self._fetch_factory('POST', '_api/user/add-virtual', search_params=data)()
577+
return UserRecord(**response.json())
578+
555579
def set_user(
556580
self,
557581
username: str,

lfss/cli/user.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ async def _main():
1919
sp_add.add_argument('--max-storage', type=parse_storage_size, default="10G", help="Maximum storage size, e.g. 1G, 100M, 10K, default is 10G")
2020

2121
sp_add_virtual = sp.add_parser('add-virtual', help="Add a virtual (hidden) user, username will be prefixed with '.v-'")
22-
sp_add_virtual.add_argument('--tag', type=str, default=None, help="Tag for the virtual user, will be embedded in the username for easier identification")
22+
sp_add_virtual.add_argument('--tag', type=str, default="", help="Tag for the virtual user, will be embedded in the username for easier identification")
2323
sp_add_virtual.add_argument('--peers', type=str, default="", help="Peer users and their access levels in the format 'READ:user1,user2;WRITE:user3'")
2424
sp_add_virtual.add_argument('--max-storage', type=parse_storage_size, default="1G", help="Maximum storage size for the virtual user, e.g. 1G, 100M, 10K, default is 1G")
25+
sp_add_virtual.add_argument('--expire', type=str, default=None, help="Expire time in seconds or a string like '1d2h3m4s'. If not provided, the user will never expire.")
2526

2627
sp_delete = sp.add_parser('delete')
2728
sp_delete.add_argument('username', type=str)
@@ -69,15 +70,16 @@ async def get_uconn():
6970
max_storage=args.max_storage,
7071
permission=args.permission
7172
)
72-
print('User created, credential:', user.credential)
73+
print('User created. | credential:', user.credential)
7374

7475
if args.subparser_name == 'add-virtual':
7576
user = await UserCtl.add_virtual(
7677
tag=args.tag,
7778
peers=args.peers,
78-
max_storage=args.max_storage
79+
max_storage=args.max_storage,
80+
expire=args.expire
7981
)
80-
print('Virtual user created, username:', user.username, ', credential:', user.credential)
82+
print('Virtual user created, username:', user.username, '| credential:', user.credential)
8183

8284
if args.subparser_name == 'delete':
8385
user = await UserCtl.delete(args.username)

lfss/eng/userman.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
High level user-management API.
33
"""
44

5-
import secrets
5+
import secrets, random
66
from typing import Optional
77
from .utils import parse_storage_size, parse_sec_time, fmt_sec_time
88
from .datatype import UserRecord, FileReadPermission, AccessLevel
@@ -100,7 +100,7 @@ async def add(
100100
if isinstance(permission, str):
101101
permission = parse_permission(permission)
102102

103-
UserCtl.logger.info(f"Creating user: {username}, admin: {admin}, max_storage: {max_storage}, permission: {permission.name}")
103+
UserCtl.logger.debug(f"Creating user: {username}, admin: {admin}, max_storage: {max_storage}, permission: {permission.name}")
104104
async with transaction() as conn:
105105
uconn = UserConn(conn)
106106
user_id = await uconn.create_user(username, password, admin, max_storage=max_storage, permission=permission)
@@ -113,7 +113,7 @@ async def add_virtual(
113113
tag: str = "",
114114
peers: dict[AccessLevel, list[str]] | str = {},
115115
max_storage: int | str = '100G',
116-
expire_seconds: Optional[int] = None,
116+
expire: Optional[int | str] = None,
117117
) -> UserRecord:
118118
"""
119119
Add a new virtual (hidden) user to the system.
@@ -122,16 +122,23 @@ async def add_virtual(
122122
if peers is a string, it will be parsed by parse_peer_list.
123123
Returns the created UserRecord.
124124
"""
125+
def rnd_part(n: int) -> str:
126+
base = secrets.token_urlsafe(n)
127+
return base.replace('-', random.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'))
128+
125129
if tag:
126-
assert tag.isalnum(), "Tag must be alphanumeric"
127-
username = f"{UserCtl.virtual_prefix}{tag}-{secrets.token_urlsafe(8)}"
130+
assert_or(tag.isalnum(), lambda: InvalidInputError("Tag must be alphanumeric"))
131+
username = f"{UserCtl.virtual_prefix}{tag}-{rnd_part(8)}"
128132
else:
129-
username = f"{UserCtl.virtual_prefix}{secrets.token_urlsafe(12)}"
133+
username = f"{UserCtl.virtual_prefix}{rnd_part(12)}"
130134

131135
if isinstance(peers, str):
132136
peers = parse_peer_list(peers)
133137

134-
UserCtl.logger.info(f"Creating virtual user: {username}, expire in {expire_seconds} seconds, peers: {peers}")
138+
if isinstance(expire, str):
139+
expire = parse_sec_time(expire)
140+
141+
UserCtl.logger.debug(f"Creating virtual user: {username}, expire in {expire} seconds, peers: {peers}")
135142
async with transaction() as conn:
136143
uconn = UserConn(conn)
137144
if isinstance(max_storage, str):
@@ -144,8 +151,8 @@ async def add_virtual(
144151
permission=FileReadPermission.UNSET,
145152
_validate = False
146153
)
147-
if expire_seconds is not None:
148-
await uconn.set_user_expire(user_id, expire_seconds)
154+
if expire is not None:
155+
await uconn.set_user_expire(user_id, expire)
149156
for level, user_list in peers.items():
150157
if isinstance(level, str):
151158
level = parse_access_level(level)
@@ -165,7 +172,7 @@ async def set_expire(username: str, expire_seconds: Optional[int | str]):
165172
if isinstance(expire_seconds, str):
166173
expire_seconds = parse_sec_time(expire_seconds)
167174
user = await _get_user__check(username)
168-
UserCtl.logger.info(
175+
UserCtl.logger.debug(
169176
f"Setting user expire: {username} to "
170177
f"{fmt_sec_time(expire_seconds) if expire_seconds is not None else 'never'}"
171178
)
@@ -181,7 +188,7 @@ async def delete(username: str) -> UserRecord:
181188
""" Delete a user from the system"""
182189
await _get_user__check(username)
183190

184-
UserCtl.logger.info(f"Deleting user: {username}")
191+
UserCtl.logger.debug(f"Deleting user: {username}")
185192
return await Database().delete_user(username)
186193

187194
@staticmethod

lfss/svc/app_native_user.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ async def add_user(
9393
permission=permission
9494
)
9595

96+
@router_user.post("/add-virtual")
97+
@handle_exception
98+
async def add_virtual_user(
99+
tag: str = "",
100+
peers: str = "",
101+
max_storage: str = '1G',
102+
expire: Optional[int | str] = None,
103+
_: UserRecord = Depends(admin_user),
104+
):
105+
# not desensitized
106+
return await UserCtl.add_virtual(
107+
tag=tag,
108+
peers=peers,
109+
max_storage=max_storage,
110+
expire=expire
111+
)
112+
96113
@router_user.post("/update")
97114
@handle_exception
98115
async def update_user(

test/cases/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
def get_conn(username, password = 'test'):
88
return Client(f"http://localhost:{SERVER_PORT}", token=hash_credential(username, password))
9+
def get_conn_bytoken(token):
10+
return Client(f"http://localhost:{SERVER_PORT}", token=token)
911

1012
def create_server_context():
1113
# clear environment variables

test/cases/test_14_virtual_user.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
import subprocess
3+
import pytest
4+
from lfss.api import AccessLevel
5+
from .common import create_server_context, get_conn, get_conn_bytoken, get_conn
6+
from ..config import SANDBOX_DIR
7+
8+
server = create_server_context()
9+
v0_name = ""
10+
v0_token = ""
11+
v1_name = ""
12+
v1_token = ""
13+
14+
15+
def test_init_user_creation(server):
16+
global v0_token, v1_token, v0_name, v1_name
17+
subprocess.check_output(['lfss-user', 'add', 'u0', 'test', "--admin"], cwd=SANDBOX_DIR)
18+
19+
u0 = get_conn('u0')
20+
u0.add_user('u1', 'test', permission='public')
21+
22+
v0 = u0.add_virtual_user(tag="session1", peers={AccessLevel.READ: ['u1']}, expire='10d')
23+
v0_token = v0.credential
24+
v0_name = v0.username
25+
26+
v1 = u0.add_virtual_user(peers=f"write:u1,{v0_name}")
27+
v1_token = v1.credential
28+
v1_name = v1.username
29+
30+
def test_v1_put(server):
31+
v1 = get_conn_bytoken(v1_token)
32+
33+
with pytest.raises(Exception, match="403"):
34+
v1.put_json(f'{v1_name}/data.json', {'message': 'Hello from v0'})
35+
36+
with pytest.raises(Exception, match="403"):
37+
v1.put_json(f'{v0_name}/data.json', {'message': 'Hello from v0'})
38+
39+
v1.put_json('u1/data_from_v1.json', {'message': 'Hello from v1'})
40+
41+
def test_admin_put(server):
42+
u0 = get_conn('u0')
43+
with pytest.raises(Exception, match="403"):
44+
u0.put_json(f'{v0_name}/data.json', {'message': 'Hello from admin to v0'})
45+
46+
def test_expire(server):
47+
u0 = get_conn('u0')
48+
v3_info = u0.add_virtual_user(tag="toexpire", expire='2s', peers={AccessLevel.WRITE: ['u1']}, max_storage=10240)
49+
50+
v3 = get_conn_bytoken(v3_info.credential)
51+
v3.put_json(f'u1/data.json', {'message': 'Hello from v3'})
52+
53+
import time
54+
time.sleep(2)
55+
56+
with pytest.raises(Exception, match="401"):
57+
v3.put_json(f'u1/data.json', {'message': 'Hello from v3'})

0 commit comments

Comments
 (0)