From 14cffa621589c46831e0ede90072252d061999d2 Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Tue, 29 Oct 2024 14:04:06 -0600 Subject: [PATCH] refactor kerberos plugin to use api_method This commit performs boiler-plate conversion of kerberos.py to new api_method schema. kerberos.config and kerberos.update are deliberately excluded at this point due to in-progress changes to ConfigService api_method plumbing. --- .../middlewared/api/v25_04_0/__init__.py | 4 + .../api/v25_04_0/activedirectory.py | 28 +++ .../middlewared/api/v25_04_0/kerberos.py | 156 ++++++++++++++++ .../api/v25_04_0/kerberos_keytab.py | 56 ++++++ .../api/v25_04_0/kerberos_realm.py | 57 ++++++ .../middlewared/plugins/activedirectory.py | 13 +- .../middlewared/plugins/kerberos.py | 167 ++++++------------ 7 files changed, 361 insertions(+), 120 deletions(-) create mode 100644 src/middlewared/middlewared/api/v25_04_0/activedirectory.py create mode 100644 src/middlewared/middlewared/api/v25_04_0/kerberos.py create mode 100644 src/middlewared/middlewared/api/v25_04_0/kerberos_keytab.py create mode 100644 src/middlewared/middlewared/api/v25_04_0/kerberos_realm.py diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index 49d052851c8f7..b7668de4e1136 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -1,4 +1,5 @@ from .acme_protocol import * # noqa +from .activedirectory import * # noqa from .alert import * # noqa from .alertservice import * # noqa from .api_key import * # noqa @@ -10,6 +11,9 @@ from .failover_reboot import * # noqa from .group import * # noqa from .iscsi_auth import * # noqa +from .kerberos import * # noqa +from .kerberos_keytab import * # noqa +from .kerberos_realm import * # noqa from .keychain import * # noqa from .privilege import * # noqa from .rdma import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/activedirectory.py b/src/middlewared/middlewared/api/v25_04_0/activedirectory.py new file mode 100644 index 0000000000000..7d16e4d3d2919 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/activedirectory.py @@ -0,0 +1,28 @@ +from middlewared.api.base import ( + BaseModel, + NonEmptyString, + single_argument_args, +) +from middlewared.utils.directoryservices.krb5_constants import ( + krb5ccache, +) +from pydantic import Field, Secret +from typing import Literal + + +__all__ = [ + 'ActivedirectoryLeaveArgs', 'ActivedirectoryLeaveResult', +] + + +class ActivedirectoryUsernamePassword(BaseModel): + username: NonEmptyString + password: Secret[NonEmptyString] + + +class ActivedirectoryLeaveArgs(BaseModel): + ad_cred: ActivedirectoryUsernamePassword + + +class ActivedirectoryLeaveResult(BaseModel): + result: Literal[True] diff --git a/src/middlewared/middlewared/api/v25_04_0/kerberos.py b/src/middlewared/middlewared/api/v25_04_0/kerberos.py new file mode 100644 index 0000000000000..84af65ac193d3 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/kerberos.py @@ -0,0 +1,156 @@ +from middlewared.api.base import ( + BaseModel, + NonEmptyString, + single_argument_args, +) +from middlewared.utils.directoryservices.krb5_constants import ( + krb5ccache, +) +from pydantic import Field, Secret +from typing import Literal + + +__all__ = [ + 'KerberosKdestroyArgs', 'KerberosKdestroyResult', + 'KerberosKinitArgs', 'KerberosKinitResult', + 'KerberosKlistArgs', 'KerberosKlistResult', + 'KerberosCheckTicketArgs', 'KerberosCheckTicketResult', + 'KerberosGetCredArgs', 'KerberosGetCredResult', +] + + +class KerberosCredentialUsernamePassword(BaseModel): + """ Private API entry defined for normalization purposes """ + username: NonEmptyString + password: Secret[NonEmptyString] + + +class KerberosCredentialKeytab(BaseModel): + """ Private API entry defined for normalization purposes """ + kerberos_principal: NonEmptyString + + +class KerberosCcacheOptions(BaseModel): + """ Private API entry defined for normalization purposes """ + ccache: Literal[ + krb5ccache.SYSTEM.value, + krb5ccache.TEMP.value, + krb5ccache.USER.value, + ] = krb5ccache.SYSTEM.value + cache_uid: int = 0 + + +class KerberosKinitKdcOverride(BaseModel): + """ Private API entry defined for normalization purposes """ + domain: str | None = None + kdc: str | None = None + libdefaults_aux: list[str] | None = None + + +class KerberosKinitOptions(KerberosCcacheOptions): + """ Private API entry defined for normalization purposes """ + renewal_period: int = 7 + lifetime: int = 0 + kdc_override: KerberosKinitKdcOverride = Field(default=KerberosKinitKdcOverride()) + + +class KerberosKlistOptions(KerberosCcacheOptions): + """ Private API entry defined for normalization purposes """ + timeout: int = 10 + + +@single_argument_args('kerberos_kinit') +class KerberosKinitArgs(BaseModel): + """ Private API entry defined for normalization purposes """ + krb5_cred: KerberosCredentialUsernamePassword | KerberosCredentialKeytab + kinit_options: KerberosKinitOptions = Field(alias='kinit-options', default=KerberosKinitOptions()) + + +class KerberosKinitResult(BaseModel): + """ Private API entry defined for normalization purposes """ + result: Literal[None] + + +class KerberosKlistArgs(BaseModel): + """ Private API entry defined for normalization purposes """ + klist_options: KerberosKlistOptions + + +class KerberosKlistEntry(BaseModel): + """ Private API entry defined for normalization purposes """ + issued: int + expires: int + renew_until: int + client: NonEmptyString + server: NonEmptyString + etype: NonEmptyString + flags: list[str] + + +class KerberosKlistFull(BaseModel): + """ Private API entry defined for normalization purposes """ + default_principal: NonEmptyString + ticket_cache: NonEmptyString + tickets: list[KerberosKlistEntry] + + +class KerberosKlistResult(BaseModel): + """ Private API entry defined for normalization purposes """ + result: KerberosKlistFull + + +class KerberosKdestroyArgs(KerberosCcacheOptions): + """ Private API entry defined for normalization purposes """ + pass + + +class KerberosKdestroyResult(BaseModel): + """ Private API entry defined for normalization purposes """ + result: Literal[None] + + +class KerberosCheckTicketArgs(BaseModel): + """ Private API entry defined for normalization purposes """ + kerberos_options: KerberosCcacheOptions = Field(alias='kerberos-options', default=KerberosCcacheOptions()) + raise_error: bool = True + + +class KerberosGssCred(BaseModel): + """ Private API entry defined for normalization purposes """ + name: NonEmptyString + name_type: NonEmptyString + name_type_oid: str + lifetime: int + + +class KerberosCheckTicketResult(BaseModel): + """ Private API entry defined for normalization purposes """ + result: KerberosGssCred + + +class ADKinitParameters(BaseModel): + """ Private API entry defined for normalization purposes """ + bindname: NonEmptyString + bindpw: Secret[NonEmptyString] + domainname: NonEmptyString + kerberos_principal: NonEmptyString + + +class LDAPKinitParameters(BaseModel): + """ Private API entry defined for normalization purposes """ + binddn: NonEmptyString | None + bindpw: Secret[NonEmptyString | None] + kerberos_realm: int + kerberos_principal: str | None + + +@single_argument_args('kerberos_get_cred') +class KerberosGetCredArgs(BaseModel): + """ Private API entry defined for normalization purposes """ + ds_type: Literal['ACTIVEDIRECTORY', 'LDAP', 'IPA'] + conf: ADKinitParameters | LDAPKinitParameters + + +class KerberosGetCredResult(BaseModel): + """ Private API entry defined for normalization purposes """ + result: KerberosCredentialUsernamePassword | KerberosCredentialKeytab diff --git a/src/middlewared/middlewared/api/v25_04_0/kerberos_keytab.py b/src/middlewared/middlewared/api/v25_04_0/kerberos_keytab.py new file mode 100644 index 0000000000000..918bf4f8bea79 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/kerberos_keytab.py @@ -0,0 +1,56 @@ +from middlewared.api.base import ( + BaseModel, + Excluded, + excluded_field, + ForUpdateMetaclass, + NonEmptyString, +) +from pydantic import Secret +from typing import Literal + + +__all__ = [ + 'KerberosKeytabEntry', + 'KerberosKeytabCreateArgs', 'KerberosKeytabCreateResult', + 'KerberosKeytabUpdateArgs', 'KerberosKeytabUpdateResult', + 'KerberosKeytabDeleteArgs', 'KerberosKeytabDeleteResult', +] + + +class KerberosKeytabEntry(BaseModel): + id: int + file: Secret[NonEmptyString] + name: NonEmptyString + + +class KerberosKeytabCreate(KerberosKeytabEntry): + id: Excluded = excluded_field() + + +class KerberosKeytabUpdate(KerberosKeytabCreate, metaclass=ForUpdateMetaclass): + pass + + +class KerberosKeytabCreateArgs(BaseModel): + kerberos_keytab_create: KerberosKeytabCreate + + +class KerberosKeytabUpdateArgs(BaseModel): + id: int + kerberos_keytab_update: KerberosKeytabUpdate + + +class KerberosKeytabCreateResult(BaseModel): + result: KerberosKeytabEntry + + +class KerberosKeytabUpdateResult(BaseModel): + result: KerberosKeytabEntry + + +class KerberosKeytabDeleteArgs(BaseModel): + id: int + + +class KerberosKeytabDeleteResult(BaseModel): + result: Literal[True] diff --git a/src/middlewared/middlewared/api/v25_04_0/kerberos_realm.py b/src/middlewared/middlewared/api/v25_04_0/kerberos_realm.py new file mode 100644 index 0000000000000..6ca168d50a862 --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/kerberos_realm.py @@ -0,0 +1,57 @@ +from middlewared.api.base import ( + BaseModel, + Excluded, + excluded_field, + ForUpdateMetaclass, + NonEmptyString, +) +from typing import Literal + + +__all__ = [ + 'KerberosRealmEntry', + 'KerberosRealmCreateArgs', 'KerberosRealmCreateResult', + 'KerberosRealmUpdateArgs', 'KerberosRealmUpdateResult', + 'KerberosRealmDeleteArgs', 'KerberosRealmDeleteResult', +] + + +class KerberosRealmEntry(BaseModel): + id: int + realm: NonEmptyString + kdc: list[str] + admin_server: list[str] + kpasswd_server: list[str] + + +class KerberosRealmCreate(KerberosRealmEntry): + id: Excluded = excluded_field() + + +class KerberosRealmUpdate(KerberosRealmCreate, metaclass=ForUpdateMetaclass): + pass + + +class KerberosRealmCreateArgs(BaseModel): + kerberos_realm_create: KerberosRealmCreate + + +class KerberosRealmUpdateArgs(BaseModel): + id: int + kerberos_realm_update: KerberosRealmUpdate + + +class KerberosRealmCreateResult(BaseModel): + result: KerberosRealmEntry + + +class KerberosRealmUpdateResult(BaseModel): + result: KerberosRealmEntry + + +class KerberosRealmDeleteArgs(BaseModel): + id: int + + +class KerberosRealmDeleteResult(BaseModel): + result: Literal[True] diff --git a/src/middlewared/middlewared/plugins/activedirectory.py b/src/middlewared/middlewared/plugins/activedirectory.py index 19d21cb4ff641..5b357cdb06ee4 100644 --- a/src/middlewared/middlewared/plugins/activedirectory.py +++ b/src/middlewared/middlewared/plugins/activedirectory.py @@ -4,6 +4,10 @@ import os import contextlib +from middlewared.api import api_method +from middlewared.api.current import ( + ActivedirectoryLeaveArgs, ActivedirectoryLeaveResult, +) from middlewared.plugins.smb import SMBCmd from middlewared.plugins.kerberos import krb5ccache from middlewared.schema import ( @@ -776,8 +780,11 @@ async def lookup_dc(self, domain=None): out = json.loads(lookup.stdout.decode()) return out - @accepts(Ref('kerberos_username_password'), roles=['DIRECTORY_SERVICE_WRITE'], audit='Active directory leave') - @returns() + @api_method( + ActivedirectoryLeaveArgs, ActivedirectoryLeaveResult, + roles=['DIRECTORY_SERVICE_WRITE'], + audit='Active directory leave', + ) @job(lock="AD_start_stop") async def leave(self, job, data): """ @@ -880,4 +887,4 @@ async def leave(self, job, data): await self.middleware.call('service.restart', 'cifs') await self.middleware.call('service.restart', 'idmap') job.set_progress(100, 'Successfully left activedirectory domain.') - return + return True diff --git a/src/middlewared/middlewared/plugins/kerberos.py b/src/middlewared/middlewared/plugins/kerberos.py index e433c31110c48..83021c47450b1 100644 --- a/src/middlewared/middlewared/plugins/kerberos.py +++ b/src/middlewared/middlewared/plugins/kerberos.py @@ -7,7 +7,22 @@ import tempfile import time -from middlewared.schema import accepts, Dict, Int, List, Patch, Str, OROperator, Password, Ref, Bool +from middlewared.api import api_method +from middlewared.api.current import ( + KerberosRealmEntry, KerberosKeytabEntry, + KerberosRealmCreateArgs, KerberosRealmCreateResult, + KerberosRealmUpdateArgs, KerberosRealmUpdateResult, + KerberosRealmDeleteArgs, KerberosRealmDeleteResult, + KerberosKeytabCreateArgs, KerberosKeytabCreateResult, + KerberosKeytabUpdateArgs, KerberosKeytabUpdateResult, + KerberosKeytabDeleteArgs, KerberosKeytabDeleteResult, + KerberosKdestroyArgs, KerberosKdestroyResult, + KerberosKinitArgs, KerberosKinitResult, + KerberosKlistArgs, KerberosKlistResult, + KerberosCheckTicketArgs, KerberosCheckTicketResult, + KerberosGetCredArgs, KerberosGetCredResult, +) +from middlewared.schema import accepts, Dict, Str from middlewared.service import CallError, ConfigService, CRUDService, job, periodic, private, ValidationErrors import middlewared.sqlalchemy as sa from middlewared.utils import run @@ -88,7 +103,6 @@ async def do_update(self, data): return await self.config() @private - @accepts(Ref('kerberos-options')) def ccache_path(self, data): krb_ccache = krb5ccache[data['ccache']] @@ -133,16 +147,7 @@ def generate_stub_config(self, realm, kdc=None, libdefaultsaux=None): krbconf.add_realms(realms) krbconf.write() - @private - @accepts( - Dict( - 'kerberos-options', - Str('ccache', enum=[x.name for x in krb5ccache], default=krb5ccache.SYSTEM.name), - Int('ccache_uid', default=0), - register=True, - ), - Bool('raise_error', default=True) - ) + @api_method(KerberosCheckTicketArgs, KerberosCheckTicketResult, private=True) def check_ticket(self, data, raise_error): """ Perform very basic test that we have a valid kerberos ticket in the @@ -266,29 +271,7 @@ async def _validate_libdefaults(self, libdefaults): return verrors - @private - @accepts(Dict( - "get-kerberos-creds", - Str("dstype", required=True, enum=[x.value for x in DSType]), - OROperator( - Dict( - 'ad_parameters', - Str('bindname'), - Str('bindpw'), - Str('domainname'), - Str('kerberos_principal') - ), - Dict( - 'ldap_parameters', - Str('binddn'), - Str('bindpw'), - Int('kerberos_realm'), - Str('kerberos_principal') - ), - name='conf', - required=True - ) - )) + @api_method(KerberosGetCredArgs, KerberosGetCredResult, private=True) async def get_cred(self, data): ''' Get kerberos cred from directory services config to use for `do_kinit`. @@ -343,39 +326,7 @@ def _dump_current_cred(self, credential, ccache_path): return None - @private - @accepts(Dict( - 'do_kinit', - OROperator( - Dict( - 'kerberos_username_password', - Str('username', required=True), - Password('password', required=True), - register=True - ), - Dict( - 'kerberos_keytab', - Str('kerberos_principal', required=True), - ), - name='krb5_cred', - required=True, - ), - Patch( - 'kerberos-options', - 'kinit-options', - ('add', {'name': 'renewal_period', 'type': 'int', 'default': 7}), - ('add', {'name': 'lifetime', 'type': 'int', 'default': 0}), - ('add', { - 'name': 'kdc_override', - 'type': 'dict', - 'args': [ - Str('domain', default=None), - Str('kdc', default=None), - List('libdefaults_aux', default=None) - ] - }), - ) - )) + @api_method(KerberosKinitArgs, KerberosKinitResult, private=True) def do_kinit(self, data): ccache = krb5ccache[data['kinit-options']['ccache']] creds = data['krb5_cred'] @@ -512,12 +463,7 @@ async def _kinit(self): cred = await self.get_cred(payload) return await self.middleware.call('kerberos.do_kinit', {'krb5_cred': cred}) - @private - @accepts(Patch( - 'kerberos-options', - 'klist-options', - ('add', {'name': 'timeout', 'type': 'int', 'default': 10}), - )) + @api_method(KerberosKlistArgs, KerberosKlistResult, private=True) async def klist(self, data): ccache = krb5ccache[data['ccache']].value @@ -529,8 +475,7 @@ async def klist(self, data): except asyncio.TimeoutError: raise CallError(f'Attempt to list kerberos tickets timed out after {data["timeout"]} seconds') - @private - @accepts(Ref('kerberos-options')) + @api_method(KerberosKdestroyArgs, KerberosKdestroyResult, private=True) async def kdestroy(self, data): kdestroy = await run(['kdestroy', '-c', krb5ccache[data['ccache']].value], check=False) if kdestroy.returncode != 0: @@ -611,6 +556,7 @@ class Config: namespace = 'kerberos.realm' cli_namespace = 'directory_service.kerberos.realm' role_prefix = 'DIRECTORY_SERVICE' + entry = KerberosRealmEntry @private async def kerberos_extend(self, data): @@ -626,20 +572,9 @@ async def kerberos_compress(self, data): return data - ENTRY = Patch( - 'kerberos_realm_create', 'kerberos_realm_entry', - ('add', Int('id')), - ) - - @accepts( - Dict( - 'kerberos_realm_create', - Str('realm', required=True), - List('kdc'), - List('admin_server'), - List('kpasswd_server'), - register=True - ), + @api_method( + KerberosRealmCreateArgs, + KerberosRealmCreateResult, audit='Kerberos realm create:', audit_extended=lambda data: data['realm'] ) @@ -671,13 +606,9 @@ async def do_create(self, data): await self.middleware.call('service.restart', 'cron') return await self.get_instance(id_) - @accepts( - Int('id', required=True), - Patch( - "kerberos_realm_create", - "kerberos_realm_update", - ("attr", {"update": True}) - ), + @api_method( + KerberosRealmUpdateArgs, + KerberosRealmUpdateResult, audit='Kerberos realm update:', audit_callback=True ) @@ -701,7 +632,12 @@ async def do_update(self, audit_callback, id_, data): await self.middleware.call('etc.generate', 'kerberos') return await self.get_instance(id_) - @accepts(Int('id'), audit='Kerberos realm delete:', audit_callback=True) + @api_method( + KerberosRealmDeleteArgs, + KerberosRealmDeleteResult, + audit='Kerberos realm delete:', + audit_callback=True + ) async def do_delete(self, audit_callback, id_): """ Delete a kerberos realm by ID. @@ -710,6 +646,7 @@ async def do_delete(self, audit_callback, id_): audit_callback(realm_name) await self.middleware.call('datastore.delete', self._config.datastore, id_) await self.middleware.call('etc.generate', 'kerberos') + return True @private async def _validate(self, data): @@ -736,19 +673,11 @@ class Config: namespace = 'kerberos.keytab' cli_namespace = 'directory_service.kerberos.keytab' role_prefix = 'DIRECTORY_SERVICE' + entry = KerberosKeytabEntry - ENTRY = Patch( - 'kerberos_keytab_create', 'kerberos_keytab_entry', - ('add', Int('id')), - ) - - @accepts( - Dict( - 'kerberos_keytab_create', - Str('file', max_length=None, private=True), - Str('name'), - register=True - ), + @api_method( + KerberosKeytabCreateArgs, + KerberosKeytabCreateResult, audit='Kerberos keytab create:', audit_extended=lambda data: data['name'] ) @@ -774,12 +703,9 @@ async def do_create(self, data): return await self.get_instance(id_) - @accepts( - Int('id', required=True), - Patch( - 'kerberos_keytab_create', - 'kerberos_keytab_update', - ), + @api_method( + KerberosKeytabUpdateArgs, + KerberosKeytabUpdateResult, audit='Kerberos keytab update:', audit_callback=True ) @@ -806,7 +732,12 @@ async def do_update(self, audit_callback, id_, data): return await self.get_instance(id_) - @accepts(Int('id'), audit='Kerberos keytab delete:', audit_callback=True) + @api_method( + KerberosKeytabDeleteArgs, + KerberosKeytabDeleteResult, + audit='Kerberos keytab delete:', + audit_callback=True + ) async def do_delete(self, audit_callback, id_): """ Delete kerberos keytab by id, and force regeneration of @@ -838,6 +769,8 @@ async def do_delete(self, audit_callback, id_): 'Failed to start kerberos service after deleting keytab entry: %s' % e ) + return True + @private async def _cleanup_kerberos_principals(self): principal_choices = await self.middleware.call('kerberos.keytab.kerberos_principal_choices')