From e2c458670639d5b2030ddd3ce3cc7e725e807f50 Mon Sep 17 00:00:00 2001 From: Christian Richter Date: Fri, 26 Jul 2024 07:49:37 +0200 Subject: [PATCH] add diff mode to init Signed-off-by: Christian Richter --- ocis/pkg/command/init.go | 8 +- ocis/pkg/init/init.go | 412 ++++++++++++++------------------------- ocis/pkg/init/structs.go | 213 ++++++++++++++++++++ 3 files changed, 363 insertions(+), 270 deletions(-) create mode 100644 ocis/pkg/init/structs.go diff --git a/ocis/pkg/command/init.go b/ocis/pkg/command/init.go index fff17382d0..bafd54f856 100644 --- a/ocis/pkg/command/init.go +++ b/ocis/pkg/command/init.go @@ -26,6 +26,12 @@ func InitCommand(cfg *config.Config) *cli.Command { Value: "ask", Usage: "Allow insecure oCIS config", }, + &cli.BoolFlag{ + Name: "diff", + Aliases: []string{"d"}, + Usage: "Show the difference between the current config and the new one", + Value: false, + }, &cli.BoolFlag{ Name: "force-overwrite", Aliases: []string{"f"}, @@ -57,7 +63,7 @@ func InitCommand(cfg *config.Config) *cli.Command { } else if insecureFlag == strings.ToLower("true") || insecureFlag == strings.ToLower("yes") || insecureFlag == strings.ToLower("y") { insecure = true } - err := ocisinit.CreateConfig(insecure, c.Bool("force-overwrite"), c.String("config-path"), c.String("admin-password")) + err := ocisinit.CreateConfig(insecure, c.Bool("force-overwrite"), c.Bool("diff"), c.String("config-path"), c.String("admin-password")) if err != nil { log.Fatalf("Could not create config: %s", err) } diff --git a/ocis/pkg/init/init.go b/ocis/pkg/init/init.go index 14c88214dc..8391e9b583 100644 --- a/ocis/pkg/init/init.go +++ b/ocis/pkg/init/init.go @@ -2,13 +2,14 @@ package init import ( "fmt" + "github.com/gofrs/uuid" "io" "log" "os" + "os/exec" "path" "time" - "github.com/gofrs/uuid" "github.com/owncloud/ocis/v2/ocis-pkg/generators" "gopkg.in/yaml.v2" ) @@ -23,216 +24,6 @@ var ( _insecureEvents = Events{TLSInsecure: true} ) -type TokenManager struct { - JWTSecret string `yaml:"jwt_secret"` -} - -type InsecureService struct { - Insecure bool -} - -type ProxyService struct { - OIDC InsecureProxyOIDC `yaml:"oidc"` - InsecureBackends bool `yaml:"insecure_backends"` - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type InsecureProxyOIDC struct { - Insecure bool `yaml:"insecure"` -} - -type LdapSettings struct { - BindPassword string `yaml:"bind_password"` -} -type LdapBasedService struct { - Ldap LdapSettings -} - -type Events struct { - TLSInsecure bool `yaml:"tls_insecure"` -} -type GraphApplication struct { - ID string `yaml:"id"` -} - -type GraphService struct { - Application GraphApplication - Events Events - Spaces InsecureService - Identity LdapBasedService - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type ServiceUserPasswordsSettings struct { - AdminPassword string `yaml:"admin_password"` - IdmPassword string `yaml:"idm_password"` - RevaPassword string `yaml:"reva_password"` - IdpPassword string `yaml:"idp_password"` -} -type IdmService struct { - ServiceUserPasswords ServiceUserPasswordsSettings `yaml:"service_user_passwords"` -} - -type SettingsService struct { - ServiceAccountIDs []string `yaml:"service_account_ids"` -} - -type FrontendService struct { - AppHandler InsecureService `yaml:"app_handler"` - Archiver InsecureService - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type OcmService struct { - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type AuthbasicService struct { - AuthProviders LdapBasedService `yaml:"auth_providers"` -} - -type AuthProviderSettings struct { - Oidc InsecureService -} -type AuthbearerService struct { - AuthProviders AuthProviderSettings `yaml:"auth_providers"` -} - -type UsersAndGroupsService struct { - Drivers LdapBasedService -} - -type ThumbnailSettings struct { - TransferSecret string `yaml:"transfer_secret"` - WebdavAllowInsecure bool `yaml:"webdav_allow_insecure"` - Cs3AllowInsecure bool `yaml:"cs3_allow_insecure"` -} - -type ThumbnailService struct { - Thumbnail ThumbnailSettings -} - -type Search struct { - Events Events - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type Audit struct { - Events Events -} - -type Sharing struct { - Events Events -} - -type StorageUsers struct { - Events Events - MountID string `yaml:"mount_id"` - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type Gateway struct { - StorageRegistry StorageRegistry `yaml:"storage_registry"` -} - -type StorageRegistry struct { - StorageUsersMountID string `yaml:"storage_users_mount_id"` -} - -type Notifications struct { - Notifications struct{ Events Events } // The notifications config has a field called notifications - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type Userlog struct { - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type AuthService struct { - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type Clientlog struct { - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -type WopiApp struct { - Secret string `yaml:"secret"` -} - -type App struct { - Insecure bool `yaml:"insecure"` -} - -type Collaboration struct { - WopiApp WopiApp `yaml:"wopi"` - App App `yaml:"app"` -} - -type Nats struct { - // The nats config has a field called nats - Nats struct { - TLSSkipVerifyClientCert bool `yaml:"tls_skip_verify_client_cert"` - } -} - -// Activitylog is the configuration for the activitylog service -type Activitylog struct { - ServiceAccount ServiceAccount `yaml:"service_account"` -} - -// ServiceAccount is the configuration for the used service account -type ServiceAccount struct { - ServiceAccountID string `yaml:"service_account_id"` - ServiceAccountSecret string `yaml:"service_account_secret"` -} - -// TODO: use the oCIS config struct instead of this custom struct -// We can't use it right now, because it would need "omitempty" on -// all elements, in order to produce a slim config file with `ocis init`. -// We can't just add these "omitempty" tags, since we want to generate -// full example configuration files with that struct, too. -// Proposed solution to get rid of this temporary solution: -// - use the oCIS config struct -// - set the needed values like below -// - marshal it to yaml -// - unmarshal it into yaml.Node -// - recurse through the nodes and delete empty / default ones -// - marshal it to yaml -type OcisConfig struct { - TokenManager TokenManager `yaml:"token_manager"` - MachineAuthAPIKey string `yaml:"machine_auth_api_key"` - SystemUserAPIKey string `yaml:"system_user_api_key"` - TransferSecret string `yaml:"transfer_secret"` - SystemUserID string `yaml:"system_user_id"` - AdminUserID string `yaml:"admin_user_id"` - Graph GraphService - Idp LdapBasedService - Idm IdmService - Collaboration Collaboration - Proxy ProxyService - Frontend FrontendService - AuthBasic AuthbasicService `yaml:"auth_basic"` - AuthBearer AuthbearerService `yaml:"auth_bearer"` - Users UsersAndGroupsService - Groups UsersAndGroupsService - Ocdav InsecureService - Ocm OcmService - Thumbnails ThumbnailService - Search Search - Audit Audit - Settings SettingsService `yaml:"settings"` - Sharing Sharing - StorageUsers StorageUsers `yaml:"storage_users"` - Notifications Notifications - Nats Nats - Gateway Gateway - Userlog Userlog - AuthService AuthService `yaml:"auth_service"` - Clientlog Clientlog - Activitylog Activitylog -} - func checkConfigPath(configPath string) error { targetPath := path.Join(configPath, configFilename) if _, err := os.Stat(targetPath); err == nil { @@ -241,6 +32,14 @@ func checkConfigPath(configPath string) error { return nil } +func configExists(configPath string) bool { + targetPath := path.Join(configPath, configFilename) + if _, err := os.Stat(targetPath); err == nil { + return true + } + return false +} + func backupOcisConfigFile(configPath string) (string, error) { sourceConfig := path.Join(configPath, configFilename) targetBackupConfig := path.Join(configPath, configFilename+"."+time.Now().Format("2006-01-02-15-04-05")+".backup") @@ -262,9 +61,25 @@ func backupOcisConfigFile(configPath string) (string, error) { } // CreateConfig creates a config file with random passwords at configPath -func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword string) error { +func CreateConfig(insecure, forceOverwrite, diff bool, configPath, adminPassword string) error { + if diff { + if forceOverwrite { + return fmt.Errorf("diff and force-overwrite flags are mutually exclusive") + } + if adminPassword != "" { + return fmt.Errorf("diff and admin-password flags are mutually exclusive") + } + } + + if configExists(configPath) { + if !forceOverwrite && !diff { + return fmt.Errorf("config file already exists, use --force-overwrite to overwrite or --diff to show diff") + } + } + err := checkConfigPath(configPath) - if err != nil && !forceOverwrite { + if err != nil && (!forceOverwrite && !diff) { + fmt.Println("off") return err } targetBackupConfig := "" @@ -279,59 +94,96 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin return err } - systemUserID := uuid.Must(uuid.NewV4()).String() - adminUserID := uuid.Must(uuid.NewV4()).String() - graphApplicationID := uuid.Must(uuid.NewV4()).String() - storageUsersMountID := uuid.Must(uuid.NewV4()).String() - serviceAccountID := uuid.Must(uuid.NewV4()).String() - - idmServicePassword, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for idm: %s", err) - } - idpServicePassword, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for idp: %s", err) - } - ocisAdminServicePassword := adminPassword - if ocisAdminServicePassword == "" { - ocisAdminServicePassword, err = generators.GenerateRandomPassword(passwordLength) + // Load old config + var oldCfg OcisConfig + if diff { + fp, err := os.ReadFile(path.Join(configPath, configFilename)) if err != nil { - return fmt.Errorf("could not generate random password for ocis admin: %s", err) + return err } + err = yaml.Unmarshal(fp, &oldCfg) } - revaServicePassword, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for reva: %s", err) - } - tokenManagerJwtSecret, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for tokenmanager: %s", err) - } - collaborationWOPISecret, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random wopi secret for collaboration service: %s", err) - } - machineAuthAPIKey, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for machineauthsecret: %s", err) - } - systemUserAPIKey, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random system user API key: %s", err) - } - revaTransferSecret, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for revaTransferSecret: %s", err) - } - thumbnailsTransferSecret, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err) - } - serviceAccountSecret, err := generators.GenerateRandomPassword(passwordLength) - if err != nil { - return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err) + var ( + systemUserID, adminUserID, graphApplicationID, storageUsersMountID, serviceAccountID string + idmServicePassword, idpServicePassword, ocisAdminServicePassword, revaServicePassword string + tokenManagerJwtSecret, collaborationWOPISecret, machineAuthAPIKey, systemUserAPIKey string + revaTransferSecret, thumbnailsTransferSecret, serviceAccountSecret string + ) + + if diff { + systemUserID = oldCfg.SystemUserID + adminUserID = oldCfg.AdminUserID + graphApplicationID = oldCfg.Graph.Application.ID + storageUsersMountID = oldCfg.Gateway.StorageRegistry.StorageUsersMountID + serviceAccountID = oldCfg.Graph.ServiceAccount.ServiceAccountID + + idmServicePassword = oldCfg.Idm.ServiceUserPasswords.IdmPassword + idpServicePassword = oldCfg.Idm.ServiceUserPasswords.IdpPassword + ocisAdminServicePassword = oldCfg.Idm.ServiceUserPasswords.AdminPassword + revaServicePassword = oldCfg.Idm.ServiceUserPasswords.RevaPassword + tokenManagerJwtSecret = oldCfg.TokenManager.JWTSecret + collaborationWOPISecret = oldCfg.Collaboration.WopiApp.Secret + machineAuthAPIKey = oldCfg.MachineAuthAPIKey + systemUserAPIKey = oldCfg.SystemUserAPIKey + revaTransferSecret = oldCfg.TransferSecret + thumbnailsTransferSecret = oldCfg.Thumbnails.Thumbnail.TransferSecret + serviceAccountSecret = oldCfg.Graph.ServiceAccount.ServiceAccountSecret + } else { + systemUserID = uuid.Must(uuid.NewV4()).String() + adminUserID = uuid.Must(uuid.NewV4()).String() + graphApplicationID = uuid.Must(uuid.NewV4()).String() + storageUsersMountID = uuid.Must(uuid.NewV4()).String() + serviceAccountID = uuid.Must(uuid.NewV4()).String() + + idmServicePassword, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for idm: %s", err) + } + idpServicePassword, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for idp: %s", err) + } + ocisAdminServicePassword = adminPassword + if ocisAdminServicePassword == "" { + ocisAdminServicePassword, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for ocis admin: %s", err) + } + } + + revaServicePassword, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for reva: %s", err) + } + tokenManagerJwtSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for tokenmanager: %s", err) + } + collaborationWOPISecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random wopi secret for collaboration service: %s", err) + } + machineAuthAPIKey, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for machineauthsecret: %s", err) + } + systemUserAPIKey, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random system user API key: %s", err) + } + revaTransferSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for revaTransferSecret: %s", err) + } + thumbnailsTransferSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err) + } + serviceAccountSecret, err = generators.GenerateRandomPassword(passwordLength) + if err != nil { + return fmt.Errorf("could not generate random password for thumbnailsTransferSecret: %s", err) + } } serviceAccount := ServiceAccount{ @@ -445,7 +297,6 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin } if insecure { - cfg.AuthBearer = AuthbearerService{ AuthProviders: AuthProviderSettings{Oidc: _insecureService}, } @@ -472,16 +323,40 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin cfg.Thumbnails.Thumbnail.WebdavAllowInsecure = true cfg.Thumbnails.Thumbnail.Cs3AllowInsecure = true } - yamlOutput, err := yaml.Marshal(cfg) if err != nil { return fmt.Errorf("could not marshall config into yaml: %s", err) } - targetPath := path.Join(configPath, configFilename) - err = os.WriteFile(targetPath, yamlOutput, 0600) - if err != nil { - return err + if diff { + fmt.Println("running in diff mode") + tmpFile := path.Join(configPath, "ocis.yaml.tmp") + err = os.WriteFile(tmpFile, yamlOutput, 0600) + if err != nil { + return err + } + fmt.Println("diff -u " + path.Join(configPath, configFilename) + " " + tmpFile) + cmd := exec.Command("diff", "-u", path.Join(configPath, configFilename), tmpFile) + stdout, _ := cmd.Output() + fmt.Println(string(stdout)) + err = os.Remove(tmpFile) + patchPath := path.Join(configPath, "ocis.config.patch") + err = os.WriteFile(patchPath, stdout, 0600) + if err != nil { + return err + } + fmt.Printf("diff written to %s\n", patchPath) + } else { + targetPath := path.Join(configPath, configFilename) + err = os.WriteFile(targetPath, yamlOutput, 0600) + if err != nil { + return err + } + printBanner(targetPath, ocisAdminServicePassword, targetBackupConfig) } + return nil +} + +func printBanner(targetPath, ocisAdminServicePassword, targetBackupConfig string) { fmt.Printf( "\n=========================================\n"+ " generated OCIS Config\n"+ @@ -495,5 +370,4 @@ func CreateConfig(insecure, forceOverwrite bool, configPath, adminPassword strin "An older config file has been backuped to\n %s\n\n", targetBackupConfig) } - return nil } diff --git a/ocis/pkg/init/structs.go b/ocis/pkg/init/structs.go new file mode 100644 index 0000000000..49725526ac --- /dev/null +++ b/ocis/pkg/init/structs.go @@ -0,0 +1,213 @@ +package init + +type TokenManager struct { + JWTSecret string `yaml:"jwt_secret"` +} + +type InsecureService struct { + Insecure bool +} + +type ProxyService struct { + OIDC InsecureProxyOIDC `yaml:"oidc"` + InsecureBackends bool `yaml:"insecure_backends"` + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type InsecureProxyOIDC struct { + Insecure bool `yaml:"insecure"` +} + +type LdapSettings struct { + BindPassword string `yaml:"bind_password"` +} +type LdapBasedService struct { + Ldap LdapSettings +} + +type Events struct { + TLSInsecure bool `yaml:"tls_insecure"` +} +type GraphApplication struct { + ID string `yaml:"id"` +} + +type GraphService struct { + Application GraphApplication + Events Events + Spaces InsecureService + Identity LdapBasedService + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type ServiceUserPasswordsSettings struct { + AdminPassword string `yaml:"admin_password"` + IdmPassword string `yaml:"idm_password"` + RevaPassword string `yaml:"reva_password"` + IdpPassword string `yaml:"idp_password"` +} +type IdmService struct { + ServiceUserPasswords ServiceUserPasswordsSettings `yaml:"service_user_passwords"` +} + +type SettingsService struct { + ServiceAccountIDs []string `yaml:"service_account_ids"` +} + +type FrontendService struct { + AppHandler InsecureService `yaml:"app_handler"` + Archiver InsecureService + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type OcmService struct { + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type AuthbasicService struct { + AuthProviders LdapBasedService `yaml:"auth_providers"` +} + +type AuthProviderSettings struct { + Oidc InsecureService +} +type AuthbearerService struct { + AuthProviders AuthProviderSettings `yaml:"auth_providers"` +} + +type UsersAndGroupsService struct { + Drivers LdapBasedService +} + +type ThumbnailSettings struct { + TransferSecret string `yaml:"transfer_secret"` + WebdavAllowInsecure bool `yaml:"webdav_allow_insecure"` + Cs3AllowInsecure bool `yaml:"cs3_allow_insecure"` +} + +type ThumbnailService struct { + Thumbnail ThumbnailSettings +} + +type Search struct { + Events Events + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type Audit struct { + Events Events +} + +type Sharing struct { + Events Events +} + +type StorageUsers struct { + Events Events + MountID string `yaml:"mount_id"` + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type Gateway struct { + StorageRegistry StorageRegistry `yaml:"storage_registry"` +} + +type StorageRegistry struct { + StorageUsersMountID string `yaml:"storage_users_mount_id"` +} + +type Notifications struct { + Notifications struct{ Events Events } // The notifications config has a field called notifications + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type Userlog struct { + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type AuthService struct { + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type Clientlog struct { + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +type WopiApp struct { + Secret string `yaml:"secret"` +} + +type App struct { + Insecure bool `yaml:"insecure"` +} + +type Collaboration struct { + WopiApp WopiApp `yaml:"wopi"` + App App `yaml:"app"` +} + +type Nats struct { + // The nats config has a field called nats + Nats struct { + TLSSkipVerifyClientCert bool `yaml:"tls_skip_verify_client_cert"` + } +} + +// Activitylog is the configuration for the activitylog service +type Activitylog struct { + ServiceAccount ServiceAccount `yaml:"service_account"` +} + +// ServiceAccount is the configuration for the used service account +type ServiceAccount struct { + ServiceAccountID string `yaml:"service_account_id"` + ServiceAccountSecret string `yaml:"service_account_secret"` +} + +// TODO: use the oCIS config struct instead of this custom struct +// We can't use it right now, because it would need "omitempty" on +// all elements, in order to produce a slim config file with `ocis init`. +// We can't just add these "omitempty" tags, since we want to generate +// full example configuration files with that struct, too. +// Proposed solution to get rid of this temporary solution: +// - use the oCIS config struct +// - set the needed values like below +// - marshal it to yaml +// - unmarshal it into yaml.Node +// - recurse through the nodes and delete empty / default ones +// - marshal it to yaml + +// OcisConfig is the configuration for the oCIS services +type OcisConfig struct { + TokenManager TokenManager `yaml:"token_manager"` + MachineAuthAPIKey string `yaml:"machine_auth_api_key"` + SystemUserAPIKey string `yaml:"system_user_api_key"` + TransferSecret string `yaml:"transfer_secret"` + SystemUserID string `yaml:"system_user_id"` + AdminUserID string `yaml:"admin_user_id"` + Graph GraphService `yaml:"graph"` + Idp LdapBasedService `yaml:"idp"` + Idm IdmService `yaml:"idm"` + Collaboration Collaboration `yaml:"collaboration"` + Proxy ProxyService `yaml:"proxy"` + Frontend FrontendService `yaml:"frontend"` + AuthBasic AuthbasicService `yaml:"auth_basic"` + AuthBearer AuthbearerService `yaml:"auth_bearer"` + Users UsersAndGroupsService `yaml:"users"` + Groups UsersAndGroupsService `yaml:"groups"` + Ocdav InsecureService `yaml:"ocdav"` + Ocm OcmService `yaml:"ocm"` + Thumbnails ThumbnailService `yaml:"thumbnails"` + Search Search `yaml:"search"` + Audit Audit `yaml:"audit"` + Settings SettingsService `yaml:"settings"` + Sharing Sharing `yaml:"sharing"` + StorageUsers StorageUsers `yaml:"storage_users"` + Notifications Notifications `yaml:"notifications"` + Nats Nats `yaml:"nats"` + Gateway Gateway `yaml:"gateway"` + Userlog Userlog `yaml:"userlog"` + AuthService AuthService `yaml:"auth_service"` + Clientlog Clientlog `yaml:"clientlog"` + Activitylog Activitylog `yaml:"activitylog"` +}