Skip to content

Commit 40b7ec6

Browse files
authored
Merge pull request ceph#57339 from phlogistonjohn/jjm-smb-login-control
smb: add login control access parameters to share resource Reviewed-by: Adam King <[email protected]> Reviewed-by: Avan Thakkar <[email protected]>
2 parents 2877643 + a44d01f commit 40b7ec6

File tree

8 files changed

+410
-0
lines changed

8 files changed

+410
-0
lines changed

doc/mgr/smb.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,21 @@ cephfs
480480
provider
481481
Optional. One of ``samba-vfs`` or ``kcephfs`` (``kcephfs`` is not yet
482482
supported) . Selects how CephFS storage should be provided to the share
483+
restrict_access
484+
Optional boolean, defaulting to false. If true the share will only permit
485+
access by users explicitly listed in ``login_control``.
486+
login_control
487+
Optional list of objects. Fields:
488+
489+
name
490+
Required string. Name of the user or group.
491+
category
492+
Optional. One of ``user`` (default) or ``group``.
493+
access
494+
One of ``read`` (alias ``r``), ``read-write`` (alias ``rw``), ``none``,
495+
or ``admin``. Specific access level to grant to the user or group when
496+
logging into this share. The ``none`` value denies access to the share
497+
regardless of the ``restrict_access`` value.
483498
custom_smb_share_options
484499
Optional mapping. Specify key-value pairs that will be directly added to
485500
the ``smb.conf`` (or equivalent) of a Samba server. Do *not* use this

src/pybind/mgr/smb/enums.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,26 @@ class ConfigNS(_StrEnum):
5454
SHARES = 'shares'
5555
USERS_AND_GROUPS = 'users_and_groups'
5656
JOIN_AUTHS = 'join_auths'
57+
58+
59+
class LoginCategory(_StrEnum):
60+
USER = 'user'
61+
GROUP = 'group'
62+
63+
64+
class LoginAccess(_StrEnum):
65+
ADMIN = 'admin'
66+
NONE = 'none'
67+
READ_ONLY = 'read'
68+
READ_ONLY_SHORT = 'r'
69+
READ_WRITE = 'read-write'
70+
READ_WRITE_SHORT = 'rw'
71+
72+
def expand(self) -> 'LoginAccess':
73+
"""Exapend abbreviated enum values into their full forms."""
74+
# the extra LoginAccess(...) calls are to appease mypy
75+
if self == self.READ_ONLY_SHORT:
76+
return LoginAccess(self.READ_ONLY)
77+
if self == self.READ_WRITE_SHORT:
78+
return LoginAccess(self.READ_WRITE)
79+
return self

src/pybind/mgr/smb/handler.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
CephFSStorageProvider,
2626
Intent,
2727
JoinSourceType,
28+
LoginAccess,
29+
LoginCategory,
2830
State,
2931
UserGroupSourceType,
3032
)
@@ -992,6 +994,8 @@ def _generate_share(
992994
'x:ceph:id': f'{share.cluster_id}.{share.share_id}',
993995
}
994996
}
997+
# extend share with user+group login access lists
998+
_generate_share_login_control(share, cfg)
995999
# extend share with custom options
9961000
custom_opts = share.cleaned_custom_smb_share_options
9971001
if custom_opts:
@@ -1000,6 +1004,42 @@ def _generate_share(
10001004
return cfg
10011005

10021006

1007+
def _generate_share_login_control(
1008+
share: resources.Share, cfg: Simplified
1009+
) -> None:
1010+
valid_users: List[str] = []
1011+
invalid_users: List[str] = []
1012+
read_list: List[str] = []
1013+
write_list: List[str] = []
1014+
admin_users: List[str] = []
1015+
for entry in share.login_control or []:
1016+
if entry.category == LoginCategory.GROUP:
1017+
name = f'@{entry.name}'
1018+
else:
1019+
name = entry.name
1020+
if entry.access == LoginAccess.NONE:
1021+
invalid_users.append(name)
1022+
continue
1023+
elif entry.access == LoginAccess.ADMIN:
1024+
admin_users.append(name)
1025+
elif entry.access == LoginAccess.READ_ONLY:
1026+
read_list.append(name)
1027+
elif entry.access == LoginAccess.READ_WRITE:
1028+
write_list.append(name)
1029+
if share.restrict_access:
1030+
valid_users.append(name)
1031+
if valid_users:
1032+
cfg['options']['valid users'] = ' '.join(valid_users)
1033+
if invalid_users:
1034+
cfg['options']['invalid users'] = ' '.join(invalid_users)
1035+
if read_list:
1036+
cfg['options']['read list'] = ' '.join(read_list)
1037+
if write_list:
1038+
cfg['options']['write list'] = ' '.join(write_list)
1039+
if admin_users:
1040+
cfg['options']['admin users'] = ' '.join(admin_users)
1041+
1042+
10031043
def _generate_config(
10041044
cluster: resources.Cluster,
10051045
shares: Iterable[resources.Share],

src/pybind/mgr/smb/resources.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
CephFSStorageProvider,
1313
Intent,
1414
JoinSourceType,
15+
LoginAccess,
16+
LoginCategory,
1517
UserGroupSourceType,
1618
)
1719
from .proto import Self, Simplified, checked
@@ -99,6 +101,19 @@ def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
99101
return rc
100102

101103

104+
@resourcelib.component()
105+
class LoginAccessEntry(_RBase):
106+
name: str
107+
category: LoginCategory = LoginCategory.USER
108+
access: LoginAccess = LoginAccess.READ_ONLY
109+
110+
def __post_init__(self) -> None:
111+
self.access = self.access.expand()
112+
113+
def validate(self) -> None:
114+
validation.check_access_name(self.name)
115+
116+
102117
@resourcelib.resource('ceph.smb.share')
103118
class RemovedShare(_RBase):
104119
"""Represents a share that has / will be removed."""
@@ -135,6 +150,8 @@ class Share(_RBase):
135150
browseable: bool = True
136151
cephfs: Optional[CephFSStorage] = None
137152
custom_smb_share_options: Optional[Dict[str, str]] = None
153+
login_control: Optional[List[LoginAccessEntry]] = None
154+
restrict_access: bool = False
138155

139156
def __post_init__(self) -> None:
140157
# if name is not given explicitly, take it from the share_id
@@ -155,6 +172,10 @@ def validate(self) -> None:
155172
if self.cephfs is None:
156173
raise ValueError('a cephfs configuration is required')
157174
validation.check_custom_options(self.custom_smb_share_options)
175+
if self.restrict_access and not self.login_control:
176+
raise ValueError(
177+
'a share with restricted access must define at least one login_control entry'
178+
)
158179

159180
@property
160181
def checked_cephfs(self) -> CephFSStorage:
@@ -163,6 +184,7 @@ def checked_cephfs(self) -> CephFSStorage:
163184

164185
@resourcelib.customize
165186
def _customize_resource(rc: resourcelib.Resource) -> resourcelib.Resource:
187+
rc.restrict_access.quiet = True
166188
rc.on_condition(_present)
167189
rc.on_construction_error(InvalidResourceError.wrap)
168190
return rc

src/pybind/mgr/smb/tests/test_handler.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,164 @@ def test_generate_config_ad(thandler):
372372
assert cfg['globals']['foo']['options']['realm'] == 'dom1.example.com'
373373

374374

375+
def test_generate_config_with_login_control(thandler):
376+
thandler.internal_store.overwrite(
377+
{
378+
'clusters.foo': {
379+
'resource_type': 'ceph.smb.cluster',
380+
'cluster_id': 'foo',
381+
'auth_mode': 'active-directory',
382+
'intent': 'present',
383+
'domain_settings': {
384+
'realm': 'dom1.example.com',
385+
'join_sources': [
386+
{
387+
'source_type': 'resource',
388+
'ref': 'foo1',
389+
}
390+
],
391+
},
392+
},
393+
'join_auths.foo1': {
394+
'resource_type': 'ceph.smb.join.auth',
395+
'auth_id': 'foo1',
396+
'intent': 'present',
397+
'auth': {
398+
'username': 'testadmin',
399+
'password': 'Passw0rd',
400+
},
401+
},
402+
'shares.foo.s1': {
403+
'resource_type': 'ceph.smb.share',
404+
'cluster_id': 'foo',
405+
'share_id': 's1',
406+
'intent': 'present',
407+
'name': 'Ess One',
408+
'readonly': False,
409+
'browseable': True,
410+
'cephfs': {
411+
'volume': 'cephfs',
412+
'path': '/',
413+
'provider': 'samba-vfs',
414+
},
415+
'login_control': [
416+
{
417+
'name': 'dom1\\alan',
418+
'category': 'user',
419+
'access': 'read',
420+
},
421+
{
422+
'name': 'dom1\\betsy',
423+
'category': 'user',
424+
'access': 'read-write',
425+
},
426+
{
427+
'name': 'dom1\\chuck',
428+
'category': 'user',
429+
'access': 'admin',
430+
},
431+
{
432+
'name': 'dom1\\ducky',
433+
'category': 'user',
434+
'access': 'none',
435+
},
436+
{
437+
'name': 'dom1\\eggbert',
438+
'category': 'user',
439+
'access': 'read',
440+
},
441+
{
442+
'name': 'dom1\\guards',
443+
'category': 'group',
444+
'access': 'read-write',
445+
},
446+
],
447+
},
448+
}
449+
)
450+
451+
cfg = thandler.generate_config('foo')
452+
assert cfg
453+
assert cfg['shares']['Ess One']['options']
454+
shopts = cfg['shares']['Ess One']['options']
455+
assert shopts['invalid users'] == 'dom1\\ducky'
456+
assert shopts['read list'] == 'dom1\\alan dom1\\eggbert'
457+
assert shopts['write list'] == 'dom1\\betsy @dom1\\guards'
458+
assert shopts['admin users'] == 'dom1\\chuck'
459+
460+
461+
def test_generate_config_with_login_control_restricted(thandler):
462+
thandler.internal_store.overwrite(
463+
{
464+
'clusters.foo': {
465+
'resource_type': 'ceph.smb.cluster',
466+
'cluster_id': 'foo',
467+
'auth_mode': 'active-directory',
468+
'intent': 'present',
469+
'domain_settings': {
470+
'realm': 'dom1.example.com',
471+
'join_sources': [
472+
{
473+
'source_type': 'resource',
474+
'ref': 'foo1',
475+
}
476+
],
477+
},
478+
},
479+
'join_auths.foo1': {
480+
'resource_type': 'ceph.smb.join.auth',
481+
'auth_id': 'foo1',
482+
'intent': 'present',
483+
'auth': {
484+
'username': 'testadmin',
485+
'password': 'Passw0rd',
486+
},
487+
},
488+
'shares.foo.s1': {
489+
'resource_type': 'ceph.smb.share',
490+
'cluster_id': 'foo',
491+
'share_id': 's1',
492+
'intent': 'present',
493+
'name': 'Ess One',
494+
'readonly': False,
495+
'browseable': True,
496+
'cephfs': {
497+
'volume': 'cephfs',
498+
'path': '/',
499+
'provider': 'samba-vfs',
500+
},
501+
'restrict_access': True,
502+
'login_control': [
503+
{
504+
'name': 'dom1\\alan',
505+
'category': 'user',
506+
'access': 'read',
507+
},
508+
{
509+
'name': 'dom1\\betsy',
510+
'category': 'user',
511+
'access': 'read-write',
512+
},
513+
{
514+
'name': 'dom1\\chuck',
515+
'category': 'user',
516+
'access': 'none',
517+
},
518+
],
519+
},
520+
}
521+
)
522+
523+
cfg = thandler.generate_config('foo')
524+
assert cfg
525+
assert cfg['shares']['Ess One']['options']
526+
shopts = cfg['shares']['Ess One']['options']
527+
assert shopts['invalid users'] == 'dom1\\chuck'
528+
assert shopts['valid users'] == 'dom1\\alan dom1\\betsy'
529+
assert shopts['read list'] == 'dom1\\alan'
530+
assert shopts['write list'] == 'dom1\\betsy'
531+
532+
375533
def test_error_result():
376534
share = smb.resources.Share(
377535
cluster_id='foo',

0 commit comments

Comments
 (0)