Skip to content

UQ-RCC/portal-resource-server

Repository files navigation

RCC Portals Resource Server

See application.sample.yml for example configuration.

Configure this behind an nginx instance like so:

location /resource/ {
    proxy_pass https://resource-server.example.com/resource/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Real-IP $remote_addr;

    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/ssl/nimrod-portal/certs/cacert.pem;
}

Usage

The "resource server" is the service that talks to the HPC. It can mint SSH certificates to impersonate any (non-operator) user.

Source Code: GitHub (https://github.com/UQ-RCC/portal-resource-server)

The resource server provides several endpoints:

  • /api/execute/{task}
  • /api/execute/{task}/on/{host}
  • /api/execute/{task}/in/{configuration}
  • /api/execute/{task}/in/{configuration}/on/{host}
  • /api/configurations

The only one ever used by the portals is /api/execute/{task}. The resource server is based off the old Strudel Web code, so they must have had some prior purpose.

Configuration

Tasks are defined via a separate JSON configuration file. See the short example below.

[
  ["Nimrod"],
  {
    "Nimrod": {
      "loginHost": null,
      "username": null,
      "messageRegexs": [
        { "pattern": "^INFO:(?P<info>.*(?:\n|\r\n?))" },
        { "pattern": "^WARN:(?P<warn>.*(?:\n|\r\n?))" },
        { "pattern": "^ERROR:(?P<error>.*(?:\n|\r\n?))" }
      ],
      "Commands": {
        "echoX": {
          "async": false,
          "cmd": "echo {x}",
          "failFatal": true,
          "formatFatal": false,
          "host": "login",
          "loop": false,
          "regex": [null],
          "requireMatch": false
        },
        "getAssignments": {
          "async": false,
          "exec": {
            "command": ["/opt/nimrod-portal/bin/shimrod.py", "portalapi", "getassignments"],
            "args": ["expname"]
          },
          "failFatal": true,
          "formatFatal": false,
          "host": "login",
          "loop": false,
          "regex": [
            "(?P<name>[a-zA-Z0-9_]+),(?P<type>[a-zA-Z0-9_]+),(?P<jsonconfig>.*),(?P<amqpuri>.*),(?P<amqpcert>.*),(?P<amqpnoverifypeer>.*),(?P<amqpnoverifyhost>.*),(?P<uri>.*),(?P<cert>.*),(?P<noverifypeer>.*),(?P<noverifyhost>.*)"
          ],
          "requireMatch": true
        }
      }
    }
  }
]

The only part that matters is the dictionary ("/1", ".[1]"). The rest is ignored. It is unclear why it is still there.

Defining Tasks

Each key is a configuration, and maps to the {configuration} parameter above. Within each configuration are a list of commands ("/1/Nimrod/Commands", ".[1].Nimrod.Commands").

Each key in the Commands list maps to the {task} parameter. Internally, the keys are converted to lowercase and the endpoints become case-insensitive.

The exec and cmd keys are of some importance.

cmd existed first and is the legacy way to add commands. It takes a string with substitutions of the form {variable_name}. After substitutions are applied, this string is passed directly to a remote bash -s process and thus is vulnerable to injection flaws. It is STRONGLY recommended to not create tasks of this form.

exec is the new way to define commands (added by yours truly). It's an object with two fields: command, and args.

  • command contains a list of strings that is directly executed on the remote system. Each string may NOT contain any substitutions. It is executed with the command:

    ssh <options> -- arg0 [arg1 [arg2 [argn...]]]
    
  • args is a list of argument names that the command expects. These are compiled into a JSON object and passed to the job's STDIN. For example, if args was ["expname"], then {"expname": "experiment1"} is passed to STDIN.

If both exec and cmd are specified, exec always takes preference.

The regex key is used to handle output. It's a Java regex with named captures that is applied to each line of the command's combined STDOUT and STDERR. If a line matches, the captured fields are put into a JSON object.

Request and Response

With the exception of {configuration}, {task}, and {host}, all parameters are passed via the query string. It is expected that everything is escaped correctly.

Responses are of the format:

{
  "userMessages": [
    "error1",
    "error2"
  ],
  "commandResult": [
    {
      "match0": "value",
      "match1": "value"
    },
    {
      "match0": "value2",
      "match1": "value2"
    }
  ]
}

userMessages is a list of error strings that should be displayed to the user. commandResult is a list of objects containing the matched fields in the regex.

Example

If an endpoint accessibleLocations were defined as follows:

{
  "accessibleLocations": {
    "async": false,
    "exec": {
      "command": ["/opt/nimrod-portal/bin/nimptool", "getdirs"],
      "args": []
    },
    "failFatal": true,
    "formatFatal": false,
    "host": "login",
    "loop": false,
    "regex": ["^\\s*(?P<path>\\S+)\\s*$"],
    "requireMatch": true
  }
}

You would get the following:

$ curl -sv localhost:8082/resource/api/execute/accessiblelocations | jq .
*   Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 8082 failed: Connection refused
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8082 (#0)
> GET /resource/api/execute/accessiblelocations HTTP/1.1
> Host: localhost:8082
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: POST, PUT, GET, OPTIONS, DELETE
< Access-Control-Allow-Headers: Authorization, Content-Type
< Access-Control-Max-Age: 3600
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Wed, 11 Dec 2019 06:57:13 GMT
< 
{ [206 bytes data]
* Connection #0 to host localhost left intact
{
  "userMessages": [],
  "commandResult": [
    {
      "path": "/home/uquser"
    },
    {
      "path": "/30days/uquser"
    },
    {
      "path": "/90days/uquser"
    },
    {
      "path": "/QRISdata/Q0123"
    },
    {
      "path": "/QRISdata/Q0234"
    },
    {
      "path": "/QRISdata/Q345"
    }
  ]
}

License

This project is licensed under the Apache License, Version 2.0:

Copyright © 2020 The University of Queensland

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.