about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPayas Relekar <payas@relekar.org>2024-03-18 17:59:16 +0530
committerPayas Relekar <payas@relekar.org>2024-04-03 00:53:24 +0530
commit427bf67bed80ee00e76fe777055cf4e67396ae79 (patch)
treef6f81c748ff8df41dd1e6fa5f6bed33b127eb636
parent9f3798cd7bb211d39fb5d57f6b1e16b6cef8a989 (diff)
nixos/ocis: init at 5.0.0
Co-authored-by: Casey Link <unnamedrambler@gmail.com>
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md2
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/services/web-apps/ocis.md113
-rw-r--r--nixos/modules/services/web-apps/ocis.nix201
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/ocis.nix217
-rw-r--r--pkgs/by-name/oc/ocis-bin/package.nix4
7 files changed, 538 insertions, 1 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index dddb2b9241ad7..858f1d2a61382 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -72,6 +72,8 @@ Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` for Pi
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
+- [ownCloud Infinite Scale Stack](https://owncloud.com/infinite-scale-4-0/), a modern and scalable rewrite of ownCloud.
+
 - [Handheld Daemon](https://github.com/hhd-dev/hhd), support for gaming handhelds like the Legion Go, ROG Ally, and GPD Win. Available as [services.handheld-daemon](#opt-services.handheld-daemon.enable).
 
 - [Guix](https://guix.gnu.org), a functional package manager inspired by Nix. Available as [services.guix](#opt-services.guix.enable).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 914a31dfe1b3a..178be2ab25c40 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1362,6 +1362,7 @@
   ./services/web-apps/nexus.nix
   ./services/web-apps/nifi.nix
   ./services/web-apps/node-red.nix
+  ./services/web-apps/ocis.nix
   ./services/web-apps/onlyoffice.nix
   ./services/web-apps/openvscode-server.nix
   ./services/web-apps/mobilizon.nix
diff --git a/nixos/modules/services/web-apps/ocis.md b/nixos/modules/services/web-apps/ocis.md
new file mode 100644
index 0000000000000..9156e927ed2d4
--- /dev/null
+++ b/nixos/modules/services/web-apps/ocis.md
@@ -0,0 +1,113 @@
+# ownCloud Infinite Scale {#module-services-ocis}
+
+[ownCloud Infinite Scale](https://owncloud.dev/ocis/) (oCIS) is an open-source,
+modern file-sync and sharing platform. It is a ground-up rewrite of the well-known PHP based ownCloud server.
+
+The server setup can be automated using
+[services.ocis](#opt-services.ocis.enable). The desktop client is packaged at
+`pkgs.owncloud-client`.
+
+## Basic usage {#module-services-ocis-basic-usage}
+
+oCIS is a golang application and does not require an HTTP server (such as nginx)
+in front of it, though you may optionally use one if you will.
+
+oCIS is configured using a combination of yaml and environment variables. It is
+recommended to familiarize yourself with upstream's available configuration
+options and deployment instructions:
+
+* [Getting Started](https://owncloud.dev/ocis/getting-started/)
+* [Configuration](https://owncloud.dev/ocis/config/)
+* [Basic Setup](https://owncloud.dev/ocis/deployment/basic-remote-setup/)
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.ocis = {
+    enable = true;
+    configDir = "/etc/ocis/config";
+  };
+}
+```
+
+This will start the oCIS server and make it available at `https://localhost:9200`
+
+However to make this configuration work you will need generate a configuration.
+You can do this with:
+
+```console
+$ nix-shell -p ocis-bin
+$ mkdir scratch/
+$ cd scratch/
+$ ocis init --config-path . --admin-password "changeme"
+```
+
+You may need to pass `--insecure true` or provide the `OCIS_INSECURE = true;` to
+[`services.ocis.environment`][mod-envFile], if TLS certificates are generated
+and managed externally (e.g. if you are using oCIS behind reverse proxy).
+
+If you want to manage the config file in your nix configuration, then it is
+encouraged to use a secrets manager like sops-nix or agenix.
+
+Be careful not to write files containing secrets to the globally readable nix
+store.
+
+Please note that current NixOS module for oCIS is configured to run in `fullstack`
+mode, which starts all the services for owncloud on single instance. This will
+start multiple ocis services and listen on multiple other ports.
+
+Current known services and their ports are as below:
+
+| Service            | Group   |  Port |
+|--------------------|---------|-------|
+| gateway            | api     |  9142 |
+| sharing            | api     |  9150 |
+| app-registry       | api     |  9242 |
+| ocdav              | web     | 45023 |
+| auth-machine       | api     |  9166 |
+| storage-system     | api     |  9215 |
+| webdav             | web     |  9115 |
+| webfinger          | web     | 46871 |
+| storage-system     | web     |  9216 |
+| web                | web     |  9100 |
+| eventhistory       | api     | 33177 |
+| ocs                | web     |  9110 |
+| storage-publiclink | api     |  9178 |
+| settings           | web     |  9190 |
+| ocm                | api     |  9282 |
+| settings           | api     |  9191 |
+| ocm                | web     |  9280 |
+| app-provider       | api     |  9164 |
+| storage-users      | api     |  9157 |
+| auth-service       | api     |  9199 |
+| thumbnails         | web     |  9186 |
+| thumbnails         | api     |  9185 |
+| storage-shares     | api     |  9154 |
+| sse                | sse     | 46833 |
+| userlog            | userlog | 45363 |
+| search             | api     |  9220 |
+| proxy              | web     |  9200 |
+| idp                | web     |  9130 |
+| frontend           | web     |  9140 |
+| groups             | api     |  9160 |
+| graph              | graph   |  9120 |
+| users              | api     |  9144 |
+| auth-basic         | api     |  9146 |
+
+## Configuration via environment variables
+
+You can also eschew the config file entirely and pass everything to oCIS via
+environment variables. For this make use of
+[`services.ocis.environment`][mod-env] for non-sensitive
+values, and
+[`services.ocis.environmentFile`][mod-envFile] for
+sensitive values.
+
+Configuration in (`services.ocis.environment`)[mod-env] overrides those from
+[`services.ocis.environmentFile`][mod-envFile] and will have highest
+precedence
+
+
+[mod-env]: #opt-services.ocis.environment
+[mod-envFile]: #opt-services.ocis.environmentFile
diff --git a/nixos/modules/services/web-apps/ocis.nix b/nixos/modules/services/web-apps/ocis.nix
new file mode 100644
index 0000000000000..b3ffec9ad9c19
--- /dev/null
+++ b/nixos/modules/services/web-apps/ocis.nix
@@ -0,0 +1,201 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  inherit (lib) types;
+  cfg = config.services.ocis;
+  defaultUser = "ocis";
+  defaultGroup = defaultUser;
+in
+{
+  options = {
+    services.ocis = {
+      enable = lib.mkEnableOption "ownCloud Infinite Scale";
+
+      package = lib.mkPackageOption pkgs "ocis-bin" { };
+
+      configDir = lib.mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/ocis/config";
+        description = lib.mdDoc ''
+          Path to directory containing oCIS config file.
+
+          Example config can be generated by `ocis init --config-path fileName --admin-password "adminPass"`.
+          Add `--insecure true` if SSL certificates are generated and managed externally (e.g. using oCIS behind reverse proxy).
+
+          Note: This directory must contain at least a `ocis.yaml`. Ensure
+          [user](#opt-services.ocis.user) has read/write access to it. In some
+          circumstances you may need to add additional oCIS configuration files (e.g.,
+          `proxy.yaml`) to this directory.
+        '';
+      };
+
+      environmentFile = lib.mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/ocis.env";
+        description = lib.mdDoc ''
+          An environment file as defined in {manpage}`systemd.exec(5)`.
+
+          Configuration provided in this file will override those from [configDir](#opt-services.ocis.configDir)/ocis.yaml.
+        '';
+      };
+
+      user = lib.mkOption {
+        type = types.str;
+        default = defaultUser;
+        example = "yourUser";
+        description = lib.mdDoc ''
+          The user to run oCIS as.
+          By default, a user named `${defaultUser}` will be created whose home
+          directory is [stateDir](#opt-services.ocis.stateDir).
+        '';
+      };
+
+      group = lib.mkOption {
+        type = types.str;
+        default = defaultGroup;
+        example = "yourGroup";
+        description = lib.mdDoc ''
+          The group to run oCIS under.
+          By default, a group named `${defaultGroup}` will be created.
+        '';
+      };
+
+      address = lib.mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Web interface address.";
+      };
+
+      port = lib.mkOption {
+        type = types.port;
+        default = 9200;
+        description = "Web interface port.";
+      };
+
+      url = lib.mkOption {
+        type = types.str;
+        default = "https://localhost:9200";
+        example = "https://some-hostname-or-ip:9200";
+        description = "Web interface address.";
+      };
+
+      stateDir = lib.mkOption {
+        default = "/var/lib/ocis";
+        type = types.str;
+        description = "ownCloud data directory.";
+      };
+
+      environment = lib.mkOption {
+        type = types.attrsOf types.str;
+        default = { };
+        description = lib.mdDoc ''
+          Extra config options.
+
+          See [the documentation](https://doc.owncloud.com/ocis/next/deployment/services/services.html) for available options.
+          See [notes for environment variables](https://doc.owncloud.com/ocis/next/deployment/services/env-var-note.html) for more information.
+
+          Note that all the attributes here will be copied to /nix/store/ and will be world readable. Options like *_PASSWORD or *_SECRET should be part of     [environmentFile](#opt-services.ocis.environmentFile) instead, and are only provided here for illustrative purpose.
+
+          Configuration here will override those from [environmentFile](#opt-services.ocis.environmentFile) and will have highest precedence, at the cost of security. Do NOT put security sensitive stuff here.
+        '';
+        example = {
+          OCIS_INSECURE = "false";
+          OCIS_LOG_LEVEL = "error";
+          OCIS_JWT_SECRET = "super_secret";
+          OCIS_TRANSFER_SECRET = "foo";
+          OCIS_MACHINE_AUTH_API_KEY = "foo";
+          OCIS_SYSTEM_USER_ID = "123";
+          OCIS_MOUNT_ID = "123";
+          OCIS_STORAGE_USERS_MOUNT_ID = "123";
+          GATEWAY_STORAGE_USERS_MOUNT_ID = "123";
+          CS3_ALLOW_INSECURE = "true";
+          OCIS_INSECURE_BACKENDS = "true";
+          TLS_INSECURE = "true";
+          TLS_SKIP_VERIFY_CLIENT_CERT = "true";
+          WEBDAV_ALLOW_INSECURE = "true";
+          IDP_TLS = "false";
+          GRAPH_APPLICATION_ID = "1234";
+          IDM_IDPSVC_PASSWORD = "password";
+          IDM_REVASVC_PASSWORD = "password";
+          IDM_SVC_PASSWORD = "password";
+          IDP_ISS = "https://localhost:9200";
+          OCIS_LDAP_BIND_PASSWORD = "password";
+          OCIS_SERVICE_ACCOUNT_ID = "foo";
+          OCIS_SERVICE_ACCOUNT_SECRET = "foo";
+          OCIS_SYSTEM_USER_API_KEY = "foo";
+          STORAGE_USERS_MOUNT_ID = "123";
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.users.${defaultUser} = lib.mkIf (cfg.user == defaultUser) {
+      group = cfg.group;
+      home = cfg.stateDir;
+      isSystemUser = true;
+      createHome = true;
+      description = "ownCloud Infinite Scale daemon user";
+    };
+
+    users.groups = lib.mkIf (cfg.group == defaultGroup) { ${defaultGroup} = { }; };
+
+    systemd = {
+      services.ocis = {
+        description = "ownCloud Infinite Scale Stack";
+        wantedBy = [ "multi-user.target" ];
+        environment = {
+          PROXY_HTTP_ADDR = "${cfg.address}:${toString cfg.port}";
+          OCIS_URL = cfg.url;
+          OCIS_CONFIG_DIR = if (cfg.configDir == null) then "${cfg.stateDir}/config" else cfg.configDir;
+          OCIS_BASE_DATA_PATH = cfg.stateDir;
+        } // cfg.environment;
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${lib.getExe cfg.package} server";
+          WorkingDirectory = cfg.stateDir;
+          User = cfg.user;
+          Group = cfg.group;
+          Restart = "always";
+          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          ReadWritePaths = [ cfg.stateDir ];
+          ReadOnlyPaths = [ cfg.configDir ];
+          MemoryDenyWriteExecute = true;
+          NoNewPrivileges = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          ProtectControlGroups = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectKernelLogs = true;
+          RestrictAddressFamilies = [
+            "AF_UNIX"
+            "AF_INET"
+            "AF_INET6"
+            "AF_NETLINK"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          LockPersonality = true;
+          SystemCallArchitectures = "native";
+        };
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [
+    bhankas
+    danth
+    ramblurr
+  ];
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 80edf70ee11c7..cc8f5959f006d 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -648,6 +648,7 @@ in {
   nvmetcfg = handleTest ./nvmetcfg.nix {};
   nzbget = handleTest ./nzbget.nix {};
   nzbhydra2 = handleTest ./nzbhydra2.nix {};
+  ocis = handleTest ./ocis.nix {};
   oh-my-zsh = handleTest ./oh-my-zsh.nix {};
   ollama = handleTest ./ollama.nix {};
   ombi = handleTest ./ombi.nix {};
diff --git a/nixos/tests/ocis.nix b/nixos/tests/ocis.nix
new file mode 100644
index 0000000000000..35461e2467490
--- /dev/null
+++ b/nixos/tests/ocis.nix
@@ -0,0 +1,217 @@
+import ./make-test-python.nix (
+  { lib, pkgs, ... }:
+
+  let
+    # this is a demo user created by IDM_CREATE_DEMO_USERS=true
+    demoUser = "einstein";
+    demoPassword = "relativity";
+
+    adminUser = "admin";
+    adminPassword = "hunter2";
+    testRunner =
+      pkgs.writers.writePython3Bin "test-runner"
+        {
+          libraries = [ pkgs.python3Packages.selenium ];
+          flakeIgnore = [ "E501" ];
+        }
+        ''
+          import sys
+          from selenium.webdriver.common.by import By
+          from selenium.webdriver import Firefox
+          from selenium.webdriver.firefox.options import Options
+          from selenium.webdriver.support.ui import WebDriverWait
+          from selenium.webdriver.support import expected_conditions as EC
+
+          options = Options()
+          options.add_argument('--headless')
+          driver = Firefox(options=options)
+
+          user = sys.argv[1]
+          password = sys.argv[2]
+          driver.implicitly_wait(20)
+          driver.get('https://localhost:9200/login')
+          wait = WebDriverWait(driver, 10)
+          wait.until(EC.title_contains("Sign in"))
+          driver.find_element(By.XPATH, '//*[@id="oc-login-username"]').send_keys(user)
+          driver.find_element(By.XPATH, '//*[@id="oc-login-password"]').send_keys(password)
+          driver.find_element(By.XPATH, '//*[@id="root"]//button').click()
+          wait.until(EC.title_contains("Personal"))
+        '';
+
+    # This was generated with `ocis init --config-path testconfig/ --admin-password "hunter2" --insecure true`.
+    testConfig = ''
+      token_manager:
+        jwt_secret: kaKYgfso*d9GA-yTM.&BTOUEuMz%Ai0H
+      machine_auth_api_key: sGWRG1JZ&qe&pe@N1HKK4#qH*B&@xLnO
+      system_user_api_key: h+m4aHPUtOtUJFKrc5B2=04C=7fDZaT-
+      transfer_secret: 4-R6AfUjQn0P&+h2+$skf0lJqmre$j=x
+      system_user_id: db180e0a-b38a-4edf-a4cd-a3d358248537
+      admin_user_id: ea623f50-742d-4fd0-95bb-c61767b070d4
+      graph:
+        application:
+          id: 11971eab-d560-4b95-a2d4-50726676bbd0
+        events:
+          tls_insecure: true
+        spaces:
+          insecure: true
+        identity:
+          ldap:
+            bind_password: ^F&Vn7@mYGYGuxr$#qm^gGy@FVq=.w=y
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      idp:
+        ldap:
+          bind_password: bv53IjS28x.nxth*%aRbE70%4TGNXbLU
+      idm:
+        service_user_passwords:
+          admin_password: hunter2
+          idm_password: ^F&Vn7@mYGYGuxr$#qm^gGy@FVq=.w=y
+          reva_password: z-%@fWipLliR8lD#fl.0teC#9QbhJ^eb
+          idp_password: bv53IjS28x.nxth*%aRbE70%4TGNXbLU
+      proxy:
+        oidc:
+          insecure: true
+        insecure_backends: true
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      frontend:
+        app_handler:
+          insecure: true
+        archiver:
+          insecure: true
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      auth_basic:
+        auth_providers:
+          ldap:
+            bind_password: z-%@fWipLliR8lD#fl.0teC#9QbhJ^eb
+      auth_bearer:
+        auth_providers:
+          oidc:
+            insecure: true
+      users:
+        drivers:
+          ldap:
+            bind_password: z-%@fWipLliR8lD#fl.0teC#9QbhJ^eb
+      groups:
+        drivers:
+          ldap:
+            bind_password: z-%@fWipLliR8lD#fl.0teC#9QbhJ^eb
+      ocdav:
+        insecure: true
+      ocm:
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      thumbnails:
+        thumbnail:
+          transfer_secret: 2%11!zAu*AYE&=d*8dfoZs8jK&5ZMm*%
+          webdav_allow_insecure: true
+          cs3_allow_insecure: true
+      search:
+        events:
+          tls_insecure: true
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      audit:
+        events:
+          tls_insecure: true
+      settings:
+        service_account_ids:
+        - df39a290-3f3e-4e39-b67b-8b810ca2abac
+      sharing:
+        events:
+          tls_insecure: true
+      storage_users:
+        events:
+          tls_insecure: true
+        mount_id: ef72cb8b-809c-4592-bfd2-1df603295205
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      notifications:
+        notifications:
+          events:
+            tls_insecure: true
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      nats:
+        nats:
+          tls_skip_verify_client_cert: true
+      gateway:
+        storage_registry:
+          storage_users_mount_id: ef72cb8b-809c-4592-bfd2-1df603295205
+      userlog:
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      auth_service:
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE
+      clientlog:
+        service_account:
+          service_account_id: df39a290-3f3e-4e39-b67b-8b810ca2abac
+          service_account_secret: .demKypQ$=pGl+yRar!#YaFjLYCr4YwE'';
+  in
+
+  {
+    name = "ocis";
+
+    meta.maintainers = with lib.maintainers; [
+      bhankas
+      ramblurr
+    ];
+
+    nodes.machine =
+      { config, ... }:
+      {
+        virtualisation.memorySize = 2048;
+        environment.systemPackages = [
+          pkgs.firefox-unwrapped
+          pkgs.geckodriver
+          testRunner
+        ];
+
+        # if you do this in production, dont put secrets in this file because it will be written to the world readable nix store
+        environment.etc."ocis/ocis.env".text = ''
+          ADMIN_PASSWORD=${adminPassword}
+          IDM_CREATE_DEMO_USERS=true
+        '';
+
+        # if you do this in production, dont put secrets in this file because it will be written to the world readable nix store
+        environment.etc."ocis/config/ocis.yaml".text = testConfig;
+
+        services.ocis = {
+          enable = true;
+          configDir = "/etc/ocis/config";
+          environment = {
+            OCIS_INSECURE = "true";
+          };
+          environmentFile = "/etc/ocis/ocis.env";
+        };
+      };
+
+    testScript = ''
+      start_all()
+      machine.wait_for_unit("ocis.service")
+      machine.wait_for_open_port(9200)
+      # wait for ocis to fully come up
+      machine.sleep(5)
+
+      with subtest("ocis bin works"):
+          machine.succeed("${lib.getExe pkgs.ocis-bin} version")
+
+      with subtest("use the web interface to log in with a demo user"):
+          machine.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner ${demoUser} ${demoPassword}")
+
+      with subtest("use the web interface to log in with the provisioned admin user"):
+          machine.succeed("PYTHONUNBUFFERED=1 systemd-cat -t test-runner test-runner ${adminUser} ${adminPassword}")
+    '';
+  }
+)
diff --git a/pkgs/by-name/oc/ocis-bin/package.nix b/pkgs/by-name/oc/ocis-bin/package.nix
index cdd12fb46e9c6..3b6988f078ea9 100644
--- a/pkgs/by-name/oc/ocis-bin/package.nix
+++ b/pkgs/by-name/oc/ocis-bin/package.nix
@@ -46,9 +46,11 @@ stdenv.mkDerivation (finalAttrs: {
     license = licenses.unfree;
     maintainers = with maintainers; [
       ramblurr
-      payas
+      bhankas
       danth
+      ramblurr
     ];
     sourceProvenance = [ sourceTypes.binaryNativeCode ];
+    mainProgram = "ocis";
   };
 })